Array (Array) in Cairo 1.0#
This article uses Cairo compiler version 2.0.0-rc0. Since Cairo is being rapidly updated, syntax may vary slightly between different versions. The content of this article will be updated to the stable version in the future.
Arrays are a very commonly used data structure that typically represents a collection of data elements of the same type. Whether it's a traditional executable program or a smart contract, arrays are used in both cases.
Basic Introduction#
Arrays in Cairo are a type exported from the core library array
and have several unique features:
- Due to the special memory model of Cairo, once a memory space is written, it cannot be overwritten. Therefore, elements in Cairo arrays cannot be modified, only read. This is different from most programming languages.
- An element can be added to the end of the array.
- 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
type is generic. 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 doesn't know what type of data a
should contain, so it throws an error. 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 the type like this:
use array::ArrayTrait;
fn main() {
let b = ArrayTrait::<usize>::new();
}
Both of the above methods are valid.
Retrieving Array Size Information#
You can retrieve the length of an array using len()
and check if an 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 code examples related to this:
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: get()
and at()
.
get() Function#
The get()
function is a relatively safe option as it returns an Option
value. If the accessed index is within the array's range, it returns Some
; if it's out of bounds, it returns None
. This allows us to use pattern matching to handle these two cases separately and avoid errors when accessing 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 {
// 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 also involves the BoxTrait
, which will be explained when discussing the official core library. For more information on copies and references, refer to Value Passing and Reference Passing in Cairo 1.0.
at() Function#
The at()
function directly returns a snapshot of the element at the specified index. Note that this only returns a snapshot of a single element, so we need to use the *
operator to extract the value behind the snapshot. Additionally, if the index is out of bounds, it will result in 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();
}
snap() Function#
The snap()
function retrieves 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 passed 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 we uncomment bar(arr)
, it will result in an error.
Deep Copy of an Array#
As the name suggests, a deep copy involves completely copying all elements, properties, and nested child 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. They are 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 clone()
method requires the Clone
trait from the official library.
As we can see from the definition of deep copy, it is a resource-intensive operation. 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 retrieve the length of the array you are working with, add elements, and 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 management 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 at a specific index
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;
// Get 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! 💪