Referensi dan Snapshot

The issue with the tuple code in previous Listing 4-3 is that we have to return the Array to the calling function so we can still use the Array after the call to calculate_length, because the Array was moved into calculate_length.

Snapshots

In the previous chapter, we talked about how Cairo's ownership system prevents us from using a variable after we've moved it, protecting us from potentially writing twice to the same memory cell. However, it's not very convenient. Let's see how we can retain ownership of the variable in the calling function using snapshots.

In Cairo, a snapshot is an immutable view of a value at a certain point in time. Recall that memory is immutable, so modifying a value actually creates a new memory cell. The old memory cell still exists, and snapshots are variables that refer to that "old" value. In this sense, snapshots are a view "into the past".

Here is how you would define and use a calculate_length function that takes a snapshot of an array as a parameter instead of taking ownership of the underlying value. In this example, the calculate_length function returns the length of the array passed as a parameter. As we're passing it as a snapshot, which is an immutable view of the array, we can be sure that the calculate_length function will not mutate the array, and ownership of the array is kept in the main function.

Filename: src/lib.cairo

fn main() {
    let mut arr1: Array<u128> = array![];
    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
    println!("The length of the array when the snapshot was taken is {}", first_length);
    println!("The current length of the array is {}", second_length);
}

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

Note: it is only possible to call the len() method on an array snapshot because it is defined as such in the ArrayTrait trait. If you try to call a method that is not defined for snapshots on a snapshot, you will get a compilation error. However, you can call methods expecting a snapshot on non-snapshot types.

Output dari program ini:

$ scarb cairo-run 
   Compiling no_listing_09_snapshots v0.1.0 (listings/ch04-understanding-ownership/no_listing_09_snapshots/Scarb.toml)
    Finished `dev` profile target(s) in 3 seconds
     Running no_listing_09_snapshots
The length of the array when the snapshot was taken is 0
The current length of the array is 1
Run completed successfully, returning []

Pertama, perhatikan bahwa semua kode tuple dalam deklarasi variabel dan nilai kembalian fungsi telah hilang. Kedua, perhatikan bahwa kita melewatkan @arr1 ke dalam calculate_length dan, dalam definisinya, kita mengambil @Array<u128> daripada Array<u128>.

Mari kita perhatikan lebih dekat panggilan fungsi di sini:

let second_length = calculate_length(@arr1); // Calculate the current length of the array

The @arr1 syntax lets us create a snapshot of the value in arr1. Because a snapshot is an immutable view of a value at a specific point in time, the usual rules of the linear type system are not enforced. In particular, snapshot variables always implement the Drop trait, never the Destruct trait, even dictionary snapshots.

Demikian pula, tanda fungsi menggunakan @ untuk menunjukkan bahwa tipe dari parameter arr adalah sebuah snapshot. Mari kita tambahkan beberapa anotasi penjelasan:

fn calculate_length(
    array_snapshot: @Array<u128> // array_snapshot is a snapshot of an Array
) -> usize {
    array_snapshot.len()
} // Here, array_snapshot goes out of scope and is dropped.
// However, because it is only a view of what the original array `arr` contains, the original `arr` can still be used.

Lingkup di mana variabel array_snapshot valid sama seperti lingkup parameter fungsi mana pun, tetapi nilai dasar dari snapshot tidak dihapus saat array_snapshot berhenti digunakan. Ketika fungsi memiliki snapshot sebagai parameter daripada nilai aktual, kita tidak perlu mengembalikan nilai untuk memberikan kembali kepemilikan dari nilai asli, karena kita tidak pernah memiliki kepemilikannya.

Desnap Operator

To convert a snapshot back into a regular variable, you can use the desnap operator *, which serves as the opposite of the @ operator.

Only Copy types can be desnapped. However, in the general case, because the value is not modified, the new variable created by the desnap operator reuses the old value, and so desnapping is a completely free operation, just like Copy.

Dalam contoh berikut, kita ingin menghitung luas persegi panjang, tetapi kita tidak ingin mengambil kepemilikan dari persegi panjang di fungsi calculate_area, karena kita mungkin ingin menggunakan persegi panjang tersebut lagi setelah panggilan fungsi. Karena fungsi kita tidak mengubah instance persegi panjang, kita dapat meneruskan snapshot dari persegi panjang ke fungsi, dan kemudian mengubah snapshot kembali menjadi nilai menggunakan operator desnap *.

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

fn main() {
    let rec = Rectangle { height: 3, width: 10 };
    let area = calculate_area(@rec);
    println!("Area: {}", area);
}

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
}

But, what happens if we try to modify something we’re passing as a snapshot? Try the code in Listing 4-4. Spoiler alert: it doesn’t work!

Filename: src/lib.cairo

#[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;
}

Listing 4-4: Attempting to modify a snapshot value

Here’s the error:

$ scarb cairo-run 
   Compiling listing_04_04 v0.1.0 (listings/ch04-understanding-ownership/listing_04_attempt_modifying_snapshot/Scarb.toml)
error: Invalid left-hand side of assignment.
 --> listings/ch04-understanding-ownership/listing_04_attempt_modifying_snapshot/src/lib.cairo:15:5
    rec.height = rec.width;
    ^********^

error: Invalid left-hand side of assignment.
 --> listings/ch04-understanding-ownership/listing_04_attempt_modifying_snapshot/src/lib.cairo:16:5
    rec.width = temp;
    ^*******^

error: could not compile `listing_04_04` due to previous error
error: `scarb metadata` exited with error

Compiler mencegah kita untuk memodifikasi nilai yang terkait dengan snapshot.

Referensi yang Dapat Diubah

We can achieve the behavior we want in Listing 4-4 by using a mutable reference instead of a snapshot. Mutable references are actually mutable values passed to a function that are implicitly returned at the end of the function, returning ownership to the calling context. By doing so, they allow you to mutate the value passed while keeping ownership of it by returning it automatically at the end of the execution. In Cairo, a parameter can be passed as mutable reference using the ref modifier.

Catatan: Di Cairo, sebuah parameter hanya dapat dilewati sebagai mutable reference menggunakan modifier ref jika variabel dideklarasikan sebagai mutable dengan mut.

In Listing 4-5, we use a mutable reference to modify the value of the height and width fields of the Rectangle instance in the flip function.

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

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

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

Listing 4-5: Use of a mutable reference to modify a value

Pertama, kita ubah rec menjadi mut. Kemudian kita lewatkan sebuah mutable reference dari rec ke dalam flip dengan ref rec, dan perbarui tanda fungsi untuk menerima sebuah mutable reference dengan ref rec: Rectangle. Hal ini membuat sangat jelas bahwa fungsi flip akan memutasi nilai dari instance Rectangle yang dilewatkan sebagai parameter.

Output dari program ini:

$ scarb cairo-run 
   Compiling listing_04_05 v0.1.0 (listings/ch04-understanding-ownership/listing_05_mutable_reference/Scarb.toml)
    Finished `dev` profile target(s) in 3 seconds
     Running listing_04_05
height: 10, width: 3
Run completed successfully, returning []

Seperti yang diharapkan, bidang height dan width dari variabel rec telah ditukar.

Small Recap

Let’s recap what we’ve discussed about the linear type system, ownership, snapshots, and references:

  • Pada setiap waktu tertentu, sebuah variabel hanya dapat dimiliki oleh satu entitas.
  • Anda dapat melewati sebuah variabel dengan-nilai (by-value), dengan-snapshot (by-snapshot), atau dengan-referensi (by-reference) ke dalam suatu fungsi.
  • Jika Anda melewatinya dengan-nilai (pass-by-value), kepemilikan dari variabel tersebut dialihkan ke dalam fungsi.
  • Jika Anda ingin tetap memegang kepemilikan dari variabel dan yakin bahwa fungsi Anda tidak akan memutasi nilainya, Anda dapat melewatinya sebagai snapshot dengan menggunakan @.
  • Jika Anda ingin tetap memegang kepemilikan dari variabel dan mengetahui bahwa fungsi Anda akan memutasi nilainya, Anda dapat melewatinya sebagai mutable reference dengan menggunakan ref.