StarknetAstro

StarknetAstro

Array (array) in 12_Cairo1.0

Array in Cairo 1.0#

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

An array is a very commonly used data structure that represents a collection of data elements of the same type. Arrays are used in both traditional executable programs and smart contracts.

Basic Introduction#

Arrays in Cairo are a type of array exported from the core library array and have several unique features:

  1. Due to the special memory model of Cairo, once a memory space is written, it cannot be overwritten, so the elements in the Cairo array cannot be modified, only read. This is different from most programming languages.
  2. An element can be added to the end of the array.
  3. An element can be removed from the beginning of the array.

Creating an Array#

All arrays are mutable variables, so the mut keyword is required:

fn create_array() -> Array<felt252> {
    let mut a = ArrayTrait::new(); 
    a.append(0);
    a.append(1);
    a
}

Arrays can contain elements of any type because the Array is a generic variable. When creating an array, the type needs to be specified.

use array::ArrayTrait;

fn main() {
    let mut a = ArrayTrait::new();
    // error: Type annotations needed.
}

In the above code, the compiler does not know what type of data the array a should contain, so an error is reported. We can specify the type like this:

use array::ArrayTrait;

fn main() {
    let mut a = ArrayTrait::new();
    a.append(1);
}

In the above code, by adding a value of type felt252 to the array, we specify that the array is of type Array<felt252>. We can also specify it like this:

use array::ArrayTrait;

fn main() {
    let b = ArrayTrait::<usize>::new(); 
}

Both of the above methods are valid.

Reading Array Size Information#

You can read the length of the array using len() and check if the array is empty using is_empty().

use array::ArrayTrait;
use debug::PrintTrait;
use option::OptionTrait;

fn main() {
    let mut a = ArrayTrait::new();

    a.append(1);

    // Check if the array is empty
    a.is_empty().print();

    // Get the length of the array
    a.len().print();
}

Adding and Removing Elements#

As mentioned earlier, in Cairo, elements can only be added to the end of the array and removed from the beginning. Let's take a look at some related code examples:

use array::ArrayTrait;
use debug::PrintTrait;
use option::OptionTrait;

fn main() {
    let mut a = ArrayTrait::new();

    // Add an element to the end
    a.append(1);

    // Remove the first element
    let k = a.pop_front();
    k.unwrap().print();
}

Adding an element to the end is straightforward. The pop_front method removes the first element and returns it as an Option value. Here, we use the unwrap method to convert the Option value back to its original type.

Accessing Array Elements#

There are two methods to access elements in an array: the get function and the at function.

The get Function#

The get function is a relatively safe option that returns an Option value. If the accessed index is within the range of the array, it returns Some; if it is out of bounds, it returns None. This way, we can use the match pattern to handle these two cases separately and avoid reading elements beyond the bounds.

use array::ArrayTrait;
use debug::PrintTrait;
use option::OptionTrait;
use box::BoxTrait;

fn main() {
    let mut a = ArrayTrait::new();

    a.append(1);
    let s = get_array(0,a);
    s.print();
}

fn get_array(index: usize, arr: Array<felt252>) -> felt252 {
    // The index is of type usize
    match arr.get(index) {
        Option::Some(x) => {
            // * is the symbol to get the original value from the copy
            // The return type is BoxTrait, so we need to use unbox to unwrap it
            *x.unbox()
        },
        Option::None(_) => {
            panic(arr)
        }
    }
}

The above code involves the use of BoxTrait, which will be explained when discussing the official core library. For information on copies and references, see Value Passing and Reference Passing in Cairo 1.0

The at Function#

The at function directly returns a snapshot of the element at the specified index. Note that this is only a snapshot of a single element, so we need to use the * operator to extract the value behind this snapshot. Also, if the index is out of bounds, it will cause a panic error, so it needs to be used with caution.

use array::ArrayTrait;
use debug::PrintTrait;
use option::OptionTrait;

fn main() {
    let mut a = ArrayTrait::new();

    a.append(100);
    let k = *a.at(0);
    k.print();
}

The span Function#

The span function returns a snapshot object of the array, which is very useful in read-only scenarios.

use core::array::SpanTrait;
use array::ArrayTrait;

fn main() {
    let mut a = ArrayTrait::new();
    a.append(100);
    let s = a.span();
}

Arrays as Function Parameters#

Arrays do not implement the Copy trait, so when an array is used as a function parameter, a move operation occurs and ownership is transferred.

use array::ArrayTrait;
fn foo(arr: Array<u128>) {}

fn bar(arr: Array<u128>) {}

fn main() {
    let mut arr = ArrayTrait::<u128>::new();
    foo(arr);
    // bar(arr);
}

In the above code, if the comment bar(arr) is uncommented, an error will occur.

Deep Copy of Arrays#

As the name suggests, a deep copy completely copies all the elements, properties, and nested sub-elements and properties of an object to create a new object. The new object has the same data as the original, but its memory address is different, making them two separate objects with identical data.

use array::ArrayTrait;
use clone::Clone;

fn foo(arr: Array<u128>) {}

fn bar(arr: Array<u128>) {}

fn main() {
    let mut arr = ArrayTrait::<u128>::new();
    let mut arr01 = arr.clone();
    foo(arr);
    bar(arr01);
}

The above code continues from the previous example. arr01 is a new array deep copied from arr. The copy requires the use of the Clone trait from the official library.

As we can see from the definition of deep copy, it is very resource-intensive. The clone method uses a loop to copy each element of the array one by one. Therefore, when executing this Cairo file, gas needs to be specified:

cairo-run --available-gas 200000 $cairo_file

Summary#

The core library exports an array type and related functions that allow you to easily get the length of the array you are working with, add elements, or retrieve elements at specific indices. Of particular interest is the ArrayTrait::get() function, which returns an Option type, meaning that if you try to access an index beyond the bounds, it will return None instead of exiting the program, allowing you to implement error handling functionality. Additionally, you can use generic types with arrays, making them easier to use compared to the old way of manually managing pointer values.

Summary of Array Member Functions#

trait ArrayTrait<T> {
    // Create an array
    fn new() -> Array<T>;
    
    // Add an element to the end of the array
    fn append(ref self: Array<T>, value: T);
    
    // Remove the first element of the array and return it as an option value
    fn pop_front(ref self: Array<T>) -> Option<T> nopanic;
    
    // Get the option value of a specific index, also returns an option type
    fn get(self: @Array<T>, index: usize) -> Option<Box<@T>>;
    
    // Get the value at a specific index
    fn at(self: @Array<T>, index: usize) -> @T;
    
    // Return the length of the array
    fn len(self: @Array<T>) -> usize;
    
    // Check if the array is empty
    fn is_empty(self: @Array<T>) -> bool;
    
    // Get a snapshot of the array
    fn span(self: @Array<T>) -> Span<T>;
}

Good luck everyone! 💪

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.