Genel Veri Türleri
We use generics to create definitions for item declarations, such as structs and functions, which we can then use with many different concrete data types. In Cairo, we can use generics when defining functions, structs, enums, traits, implementations and methods. In this chapter, we are going to take a look at how to effectively use generic types with all of them.
Generics allow us to write reusable code that works with many types, thus avoiding code duplication, while enhancing code maintainability.
Genel Fonksiyonlar
Making a function generic means it can operate on different types, avoiding the need for multiple, type-specific implementations. This leads to significant code reduction and increases the flexibility of the code.
Genel türleri kullanan bir fonksiyon tanımlarken, genel türleri fonksiyon imzasına yerleştiririz, genellikle parametre ve dönüş değerinin veri tiplerini belirttiğimiz yerde. Örneğin, iki Array
öğesi verildiğinde en büyüğünü döndüren bir fonksiyon oluşturmak istediğimizi hayal edin. Bu işlemi farklı türlerdeki listeler için gerçekleştirmemiz gerekiyorsa, her seferinde fonksiyonu yeniden tanımlamamız gerekirdi. Neyse ki, genel türleri kullanarak fonksiyonu bir kez uygulayabilir ve diğer görevlere geçebiliriz.
// Specify generic type T between the angulars
fn largest_list<T>(l1: Array<T>, l2: Array<T>) -> Array<T> {
if l1.len() > l2.len() {
l1
} else {
l2
}
}
fn main() {
let mut l1 = array![1, 2];
let mut l2 = array![3, 4, 5];
// There is no need to specify the concrete type of T because
// it is inferred by the compiler
let l3 = largest_list(l1, l2);
}
The largest_list
function compares two lists of the same type and returns the one with more elements and drops the other. If you compile the previous code, you will notice that it will fail with an error saying that there are no traits defined for dropping an array of a generic type. This happens because the compiler has no way to guarantee that an Array<T>
is droppable when executing the main
function. In order to drop an array of T
, the compiler must first know how to drop T
. This can be fixed by specifying in the function signature of largest_list
that T
must implement the Drop
trait. The correct function definition of largest_list
is as follows:
fn largest_list<T, impl TDrop: Drop<T>>(l1: Array<T>, l2: Array<T>) -> Array<T> {
if l1.len() > l2.len() {
l1
} else {
l2
}
}
The new largest_list
function includes in its definition the requirement that whatever generic type is placed there, it must be droppable. This is what we call trait bounds. The main
function remains unchanged, the compiler is smart enough to deduce which concrete type is being used and if it implements the Drop
trait.
Genel Türler için Kısıtlamalar
When defining generic types, it is useful to have information about them. Knowing which traits a generic type implements allows us to use it more effectively in a function's logic at the cost of constraining the generic types that can be used with the function. We saw an example of this previously by adding the TDrop
implementation as part of the generic arguments of largest_list
. While TDrop
was added to satisfy the compiler's requirements, we can also add constraints to benefit our function logic.
Bir T
genel türünden elemanların listesini verdiğimizde, aralarından en küçüğünü bulmak istediğimizi hayal edin. Başlangıçta, bir T
türünden elemanın karşılaştırılabilir olması için PartialOrd
traitini uygulaması gerektiğini biliyoruz. Sonuç fonksiyon şöyle olurdu:
// Given a list of T get the smallest one
// The PartialOrd trait implements comparison operations for T
fn smallest_element<T, impl TPartialOrd: PartialOrd<T>>(list: @Array<T>) -> T {
// This represents the smallest element through the iteration
// Notice that we use the desnap (*) operator
let mut smallest = *list[0];
// The index we will use to move through the list
let mut index = 1;
// Iterate through the whole list storing the smallest
while index < list.len() {
if *list[index] < smallest {
smallest = *list[index];
}
index = index + 1;
};
smallest
}
fn main() {
let list: Array<u8> = array![5, 3, 10];
// We need to specify that we are passing a snapshot of `list` as an argument
let s = smallest_element(@list);
assert!(s == 3);
}
The smallest_element
function uses a generic type T
that implements the PartialOrd
trait, takes a snapshot of an Array<T>
as a parameter and returns a copy of the smallest element. Because the parameter is of type @Array<T>
, we no longer need to drop it at the end of the execution and so we are not required to implement the Drop
trait for T
as well. Why does it not compile then?
When indexing on list
, the value results in a snap of the indexed element, and unless PartialOrd
is implemented for @T
we need to desnap the element using *
. The *
operation requires a copy from @T
to T
, which means that T
needs to implement the Copy
trait. After copying an element of type @T
to T
, there are now variables with type T
that need to be dropped, requiring T
to implement the Drop
trait as well. We must then add both Drop
and Copy
traits implementation for the function to be correct. After updating the smallest_element
function the resulting code would be:
fn smallest_element<T, impl TPartialOrd: PartialOrd<T>, impl TCopy: Copy<T>, impl TDrop: Drop<T>>(
list: @Array<T>,
) -> T {
let mut smallest = *list[0];
let mut index = 1;
while index < list.len() {
if *list[index] < smallest {
smallest = *list[index];
}
index = index + 1;
};
smallest
}
Anonymous Generic Implementation Parameter (+
Operator)
Şimdiye kadar, gereken genel özelliklerin her biri için bir isim belirttik: PartialOrd<T>
için TPartialOrd
, Drop<T>
için TDrop
ve Copy<T>
için TCopy
.
Ancak, çoğu zaman, fonksiyon gövdesinde uygulamayı kullanmayız; sadece bir kısıtlama olarak kullanırız. Bu durumlarda, genel türün bir özelliği uygulaması gerektiğini belirtmek için +
operatörünü kullanabilir ve uygulamaya bir isim vermeyebiliriz. Buna anonim genel uygulama parametresi denir.
+PartialOrd<T>
, impl TPartialOrd: PartialOrd<T>
ile eşdeğerdir.
smallest_element
fonksiyonun imzasını şu şekilde yeniden yazabiliriz:
fn smallest_element<T, +PartialOrd<T>, +Copy<T>, +Drop<T>>(list: @Array<T>) -> T {
let mut smallest = *list[0];
let mut index = 1;
loop {
if index >= list.len() {
break smallest;
}
if *list[index] < smallest {
smallest = *list[index];
}
index = index + 1;
}
}
Structs
We can also define structs to use a generic type parameter for one or more fields using the <>
syntax, similar to function definitions. First, we declare the name of the type parameter inside the angle brackets just after the name of the struct. Then we use the generic type in the struct definition where we would otherwise specify concrete data types. The next code example shows the definition Wallet<T>
which has a balance
field of type T
.
#[derive(Drop)]
struct Wallet<T> {
balance: T,
}
fn main() {
let w = Wallet { balance: 3 };
}
Yukarıdaki kod, Wallet
türü için Drop
özelliğini otomatik olarak türetilir. Bu, aşağıdaki kodu yazmakla eşdeğerdir:
struct Wallet<T> {
balance: T,
}
impl WalletDrop<T, +Drop<T>> of Drop<Wallet<T>>;
fn main() {
let w = Wallet { balance: 3 };
}
Wallet
için Drop
uygulamasının derive
makrosunu kullanmaktan kaçınıyoruz ve bunun yerine kendi WalletDrop
uygulamamızı tanımlıyoruz. Fonksiyonlar gibi, WalletDrop
için ek bir genel tür tanımlamamız gerektiğini unutmayın, T
'nin Drop
özelliğini uyguladığını söyleyin. Esasında, Wallet<T>
yapısının, T
bırakılabilir olduğu sürece bırakılabilir olduğunu söylüyoruz.
Son olarak, Wallet
'a, T
'den farklı ancak aynı zamanda genel olan bir adres alanı eklemek istiyorsak, <>
arasına başka bir genel tür ekleyebiliriz:
#[derive(Drop)]
struct Wallet<T, U> {
balance: T,
address: U,
}
fn main() {
let w = Wallet { balance: 3, address: 14 };
}
We add to the Wallet
struct definition a new generic type U
and then assign this type to the new field member address
. Notice that the derive
attribute for the Drop
trait works for U
as well.
Enumlar
Yapılarla yaptığımız gibi, enumları da varyantlarında genel veri türlerini tutacak şekilde tanımlayabiliriz. Örneğin, Cairo çekirdek kütüphanesi tarafından sağlanan Option<T>
enumu:
enum Option<T> {
Some: T,
None,
}
Option<T>
enumu, T
türü üzerinden genelleştirilmiştir ve iki varyanta sahiptir: Some
, T
türünde bir değer tutar ve None
herhangi bir değer tutmaz. Option<T>
enumunu kullanarak, bir değerin isteğe bağlı kavramını soyut bir şekilde ifade edebiliriz ve çünkü değer genel bir T
türündedir, bu soyutlamayı her türle kullanabiliriz.
Enums can use multiple generic types as well, like the definition of the Result<T, E>
enum that the core library provides:
enum Result<T, E> {
Ok: T,
Err: E,
}
Result<T, E>
enumu iki genel tür, T
ve E
içerir ve iki varyanta sahiptir: Ok
, T
türünde bir değer tutar ve Err
, E
türünde bir değer tutar. Bu tanım, bir işlemin başarılı olabileceği (bir T
türünde değer döndürerek) veya başarısız olabileceği (bir E
türünde değer döndürerek) herhangi bir yerde Result
enumunu kullanmayı uygun hale getirir.
Genel Metodlar
We can implement methods on structs and enums, and use the generic types in their definitions, too. Using our previous definition of Wallet<T>
struct, we define a balance
method for it:
#[derive(Copy, Drop)]
struct Wallet<T> {
balance: T,
}
trait WalletTrait<T> {
fn balance(self: @Wallet<T>) -> T;
}
impl WalletImpl<T, +Copy<T>> of WalletTrait<T> {
fn balance(self: @Wallet<T>) -> T {
return *self.balance;
}
}
fn main() {
let w = Wallet { balance: 50 };
assert!(w.balance() == 50);
}
We first define WalletTrait<T>
trait using a generic type T
which defines a method that returns the value of the field balance
from Wallet
. Then we give an implementation for the trait in WalletImpl<T>
. Note that you need to include a generic type in both definitions of the trait and the implementation.
We can also specify constraints on generic types when defining methods on the type. We could, for example, implement methods only for Wallet<u128>
instances rather than Wallet<T>
. In the code example, we define an implementation for wallets which have a concrete type of u128
for the balance
field.
#[derive(Copy, Drop)]
struct Wallet<T> {
balance: T,
}
/// Generic trait for wallets
trait WalletTrait<T> {
fn balance(self: @Wallet<T>) -> T;
}
impl WalletImpl<T, +Copy<T>> of WalletTrait<T> {
fn balance(self: @Wallet<T>) -> T {
return *self.balance;
}
}
/// Trait for wallets of type u128
trait WalletReceiveTrait {
fn receive(ref self: Wallet<u128>, value: u128);
}
impl WalletReceiveImpl of WalletReceiveTrait {
fn receive(ref self: Wallet<u128>, value: u128) {
self.balance += value;
}
}
fn main() {
let mut w = Wallet { balance: 50 };
assert!(w.balance() == 50);
w.receive(100);
assert!(w.balance() == 150);
}
The new method receive
increments the size of balance
of any instance of a Wallet<u128>
. Notice that we changed the main
function making w
a mutable variable in order for it to be able to update its balance. If we were to change the initialization of w
by changing the type of balance
the previous code wouldn't compile.
Cairo allows us to define generic methods inside generic traits as well. Using the past implementation from Wallet<U, V>
we are going to define a trait that picks two wallets of different generic types and creates a new one with a generic type of each. First, let's rewrite the struct definition:
struct Wallet<T, U> {
balance: T,
address: U,
}
Next, we are going to naively define the mixup trait and implementation:
// This does not compile!
trait WalletMixTrait<T1, U1> {
fn mixup<T2, U2>(self: Wallet<T1, U1>, other: Wallet<T2, U2>) -> Wallet<T1, U2>;
}
impl WalletMixImpl<T1, U1> of WalletMixTrait<T1, U1> {
fn mixup<T2, U2>(self: Wallet<T1, U1>, other: Wallet<T2, U2>) -> Wallet<T1, U2> {
Wallet { balance: self.balance, address: other.address }
}
}
We are creating a trait WalletMixTrait<T1, U1>
with the mixup<T2, U2>
method which given an instance of Wallet<T1, U1>
and Wallet<T2, U2>
creates a new Wallet<T1, U2>
. As mixup
signature specifies, both self
and other
are getting dropped at the end of the function, which is why this code does not compile. If you have been following from the start until now you would know that we must add a requirement for all the generic types specifying that they will implement the Drop
trait for the compiler to know how to drop instances of Wallet<T, U>
. The updated implementation is as follows:
trait WalletMixTrait<T1, U1> {
fn mixup<T2, +Drop<T2>, U2, +Drop<U2>>(
self: Wallet<T1, U1>, other: Wallet<T2, U2>,
) -> Wallet<T1, U2>;
}
impl WalletMixImpl<T1, +Drop<T1>, U1, +Drop<U1>> of WalletMixTrait<T1, U1> {
fn mixup<T2, +Drop<T2>, U2, +Drop<U2>>(
self: Wallet<T1, U1>, other: Wallet<T2, U2>,
) -> Wallet<T1, U2> {
Wallet { balance: self.balance, address: other.address }
}
}
WalletMixImpl
deklarasyonunda T1
ve U1
'in bırakılabilir olması gerektiği gereksinimlerini ekliyoruz. Sonra aynısını T2
ve U2
için yapıyoruz, bu sefer mixup
imzasının bir parçası olarak. Artık mixup
fonksiyonunu deneyebiliriz:
fn main() {
let w1: Wallet<bool, u128> = Wallet { balance: true, address: 10 };
let w2: Wallet<felt252, u8> = Wallet { balance: 32, address: 100 };
let w3 = w1.mixup(w2);
assert!(w3.balance);
assert!(w3.address == 100);
}
İlk olarak, biri Wallet<bool, u128>
ve diğeri Wallet<felt252, u8>
olmak üzere iki örnek oluşturuyoruz. Sonra, mixup
'ı çağırıp yeni bir Wallet<bool, u8>
örneği oluşturuyoruz.