Learning Cairo - Snapshots and Mutable References (7)

Learning Cairo - Snapshots and Mutable References (7)

Prerequisite

Install cairo-run.

Snapshots

Assuming we have a calculate_length function to calculate the length of an array and we need to pass the array itself as a parameter to the function. This operation will transfer the ownership of the array to the function, and if we still need to use the array after calling the function, we need to return the ownership of the array to the calling location at the end of the calculate_length function.

Fortunately, Cairo provides us with a mechanism called snapshots. In Cairo, a snapshot refers to an immutable view of data at a certain point in time. With the snapshot mechanism, we can pass a snapshot of the array into the function, and the ownership of the array is not moved, so we don't have to return the ownership at the end of the function. Let's take a look at the example below:

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

fn main() {
    let mut arr1 = ArrayTrait::<u128>::new();
    let first_snapshot = @arr1; // Take a snapshot of `arr1` at this point in time
    arr1.append(1); // Mutate `arr1` by appending a value
    let first_length = calculate_length(
        first_snapshot
    ); // Calculate the length of the array when the snapshot was taken
    let second_length = calculate_length(@arr1); // Calculate the current length of the array
    first_length.print();
    second_length.print();
}

fn calculate_length(arr: @Array<u128>) -> usize {
    arr.len()
}

Let's first look at the calculate_length function, which receives a parameter of type @Array<u128>, which indicates that it receives a snapshot of type Array<u128>. Therefore, the calling location also needs to pass in a snapshot type.

let first_snapshot = @arr1;

Here, we can create a snapshot variable of the arr1 variable by using @arr1. As we mentioned earlier, a snapshot is an immutable view of data, so the subsequent changes to the arr1 variable will not affect the snapshot taken earlier.

Since the function calculate_length receives a snapshot type as a parameter, we need to pass in a snapshot type, which can be created in advance and passed in, or created directly at the calling location:

let first_snapshot = @arr1;
let first_length = calculate_length(
    first_snapshot
);

let second_length = calculate_length(@arr1);

As the changes to the variable itself will not affect the snapshot created earlier, we don't have to worry about the subsequent changes to the variable corresponding to the snapshot.

Try running the above code, and the result is:

[DEBUG]                                 (raw: 0)

[DEBUG]                                 (raw: 1)

Run completed successfully, returning []

This is the expected result.

For functions that receive snapshot types as parameters, we do not need to explicitly return the ownership of the parameter, because the function never owned it in the first place.

Cairo also provides us with a method to convert snapshot types to regular types. We can use the * operator to perform a desnap operation, provided that the type is copyable. For array types, this operation cannot be performed because the Copy trait has not been implemented. Let's take a look at an example:

use debug::PrintTrait;

#[derive(Copy, Drop)]
struct Rectangle {
    height: u64,
    width: u64,
}

fn main() {
    let rec = Rectangle { height: 3, width: 10 };
    let area = calculate_area(@rec);
    area.print();
}

fn calculate_area(rec: @Rectangle) -> u64 {
    // As rec is a snapshot to a Rectangle, its fields are also snapshots of the fields types.
    // We need to transform the snapshots back into values using the desnap operator `*`.
    // This is only possible if the type is copyable, which is the case for u64.
    // Here, `*` is used for both multiplying the height and width and for desnapping the snapshots.
    *rec.height * *rec.width
}

In the calculate_area function, since we did not change the value of rec, we can use the snapshot type @Rectangle of the Rectangle variable as the function parameter. In the function, we use the * operator to obtain the value corresponding to the snapshot.

If we try to change the snapshot type:

// does_not_compile
#[derive(Copy, Drop)]
struct Rectangle {
    height: u64,
    width: u64,
}

fn main() {
    let rec = Rectangle { height: 3, width: 10 };
    flip(@rec);
}

fn flip(rec: @Rectangle) {
    let temp = rec.height;
    rec.height = rec.width;
    rec.width = temp;
}

We will get an error:

error: Invalid left-hand side of assignment.
 --> main.cairo:15:5
    rec.height = rec.width;
    ^********^

error: Invalid left-hand side of assignment.
 --> main.cairo:16:5
    rec.width = temp;
    ^*******^

Mutable References

If we want to implement the function of changing the parameter value in the function, Cairo also provides us with a convenient mechanism. We can use a method called mutable references to transfer both the value and ownership of the mutable type to the called function, and the ownership of the parameter will be implicitly returned to the calling location at the end of the called function. This way, we don't have to write the code to return the ownership manually.

In Cairo, if we want to use a mutable reference, we need to mark the parameter as ref. Let's take a look at an example:

use debug::PrintTrait;
#[derive(Copy, Drop)]
struct Rectangle {
    height: u64,
    width: u64,
}

fn main() {
    let mut rec = Rectangle { height: 3, width: 10 };
    flip(ref rec);
    rec.height.print();
    rec.width.print();
}

fn flip(ref rec: Rectangle) {
    let temp = rec.height;
    rec.height = rec.width;
    rec.width = temp;
}

First, remember that if we want to use ref to pass in a mutable reference, the parameter itself must be of type mut, indicating that the parameter itself is mutable. Try to compile and run, and it is confirmed that the value has indeed been changed:

[DEBUG]                         (raw: 10)

[DEBUG]                         (raw: 3)

Summary

In this section, we learned about snapshots and mutable references. Snapshots can provide an immutable view of a variable, and subsequent changes to the corresponding value will not affect the snapshot itself. We can use @ to create a snapshot.

Mutable references transfer both the value and ownership of the variable to the called function, and the ownership of the parameter is implicitly returned to the calling location at the end of the called function, which can avoid us writing the code to return the ownership manually. Note that mutable references can only be applied to mut variables.

Low latency and free Starknet node awaits!

For a limited time, Reddio is offering unrestricted access to its high-speed StarkNet Node, completely free of charge. This is an unparalleled opportunity to experience the fastest connection with the lowest delay. All you need to do is register an account on Reddio at https://dashboard.reddio.com/ and start exploring the limitless possibilities.

You can discover why Reddio claims the fastest connection by reading more here.