StarknetAstro

StarknetAstro

16_Generic in Cairo 1.0

Generic in Cairo 1.0#

The Cairo compiler version used in this article is 2.0.0-rc0. Since Cairo is being updated rapidly, there may be slight differences in syntax between different versions, and the content of the article will be updated to the stable version in the future.

Generic is a programming language feature that allows the use of type parameters when writing code, and these type parameters can be replaced with concrete types when the code is instantiated.

In actual programming, we design a set of algorithms to efficiently handle certain business problems. Without generics, each type would need to copy the same set of algorithm code. Ideally, algorithms should be independent of data structures and types, and various special data types should do their own work, while algorithms only focus on a standard implementation.

So, generics are cool 😎

In Cairo, generics can be used in functions, structs, enums, and methods in traits.

Generic in Functions#

If a function takes parameters that contain generics, the generics need to be declared in the <> before the parameters, and this will be part of the function signature. Let's implement a function that finds the smallest generic element in a generic array:

use debug::PrintTrait;
use array::ArrayTrait;

// PartialOrd implements comparison between generic variables
fn smallest_element<T, impl TPartialOrd: PartialOrd<T>, impl TCopy: Copy<T>, impl TDrop: Drop<T>>(
    list: @Array<T>
) -> T {
    // * is used here, so T must implement the copy trait
    let mut smallest = *list[0];

    let mut index = 1;

    loop {
        if index >= list.len() {
            break smallest;
        }
        // Comparison between two generic variables, requires implementation of PartialOrd
        if *list[index] < smallest {
            smallest = *list[index];
        }
        index = index + 1;
    }
}

fn main() {
    let mut list: Array<u8> = ArrayTrait::new();
    list.append(5);
    list.append(3);
    list.append(10);

    let s = smallest_element(@list);
    assert(s == 3, 0);
    s.print();
}

As you can see, in the generic declaration area, we have added many modifiers to the generic T (which can be named T or other names). Generics can be any data type, and if the data type can meet the requirements of the generic algorithm, it will inevitably have certain constraints on the passed-in data type. The impl added in <> is the constraint on the generic passed to this function.

  1. First, we took a snapshot of T's value, so T must implement the Copy trait;
  2. Secondly, the T type variable smallest is ultimately the return value of the function, returned to the main function, which includes the move operation and the Drop operation, so it needs to implement the Drop trait;
  3. Finally, we need to compare two generics, so we need to implement the PartialOrd trait.

So we see this part in the function declaration: <T, impl TPartialOrd: PartialOrd<T>, impl TCopy: Copy<T>, impl TDrop: Drop<T>>. When calling this function, all elements in the parameter array must implement the traits described in these three constraints.

Generic in Structs#

Generic fields can also be placed in struct elements, such as:

struct Wallet<T> {
    balance: T
}

impl WalletDrop<T, impl TDrop: Drop<T>> of Drop<Wallet<T>>;
#[derive(Drop)]
struct Wallet<T> {
    balance: T
}

Both of the above methods should work. The Cairo book says that the second method does not declare the type T as implementing the Drop trait, but it does not provide any example code. I have experimented a few times and have not found any differences so far. I will add it later if I find out.

Generic in Struct Methods#

In the following code, we can see that generics need to be declared in struct, trait, and impl, and constraints need to be added in impl because it stores the algorithm logic.

use debug::PrintTrait;

#[derive(Copy,Drop)]
struct Wallet<T> {
    balance: T
}

trait WalletTrait<T> {
    fn balance(self: @Wallet<T>) -> T;
}

impl WalletImpl<T, impl TCopy: Copy<T>> of WalletTrait<T>{
    fn balance(self: @Wallet<T>) -> T{
        *self.balance
    }
}

fn main() {
    let w = Wallet{balance:'100 000 000'};

    w.balance().print();
}

Here's another example that uses two different generics at the same time:

use debug::PrintTrait;

#[derive(Copy,Drop)]
struct Wallet<T, U> {
    balance: T,
    address: U,
}

trait WalletTrait<T, U> {
    fn getAll(self: @Wallet<T, U>) -> (T, U);
}

impl WalletImpl<T, impl TCopy: Copy<T>, U, impl UCopy: Copy<U>> of WalletTrait<T, U>{
    fn getAll(self: @Wallet<T, U>) -> (T, U){
        (*self.balance,*self.address)
    }
}

fn main() {
    let mut w = Wallet{
        balance: 100,
        address: '0x0000aaaaa'
    };

    let (b,a) = w.getAll();
    b.print();
    a.print();
}

Generic in Enums#

Option is an enum that uses generics:

enum Option<T> {
    Some: T,
    None: (),
}
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.