StarknetAstro

StarknetAstro

16_Generic in Cairo1.0

Generics in Cairo 1.0#

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

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

In practical programming, we design algorithms to efficiently solve certain business problems. Without generics, each type would require a separate copy of the same set of algorithm code. Ideally, algorithms should be independent of data structures and types, and different types should do their own work, while the algorithm only cares about a standard implementation.

So, generics are cool 😎

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

Generics in Functions#

If a function takes a parameter that contains generics, the generics need to be declared in the <> before the parameter, 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 {
    // Here we use *, so T must implement the copy trait
    let mut smallest = *list[0];

    let mut index = 1;

    loop {
        if index >= list.len() {
            break smallest;
        }
        // This is a comparison between two generic variables, so PartialOrd must be implemented
        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 (it can be named T or any other name). Generics can be any data type, and if the data type can meet the requirements of the generic algorithm, it will inevitably have some constraints on the data type passed to this function, and the impl added in <> is the constraint on the generics passed to this function.

  1. First, we take a value from the snapshot of T, 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 the PartialOrd trait must be implemented.

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.

Generics 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 it.

Using Generics in Struct Methods#

In the following code, we can see that generics need to be declared in the struct, trait, and impl, and constraints need to be added in the 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();
}

Generics in Enums#

Option is an example of 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.