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 the execution of the program. Recall that memory is immutable, so modifying a variable actually fills 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_area
function that takes a snapshot of a Rectangle
struct as a parameter instead of taking ownership of the underlying value. In this example, the calculate_area
function returns the area of the Rectangle
passed as a snapshot. Since we’re passing it as an immutable view, we can be sure that calculate_area
will not mutate the Rectangle
, and ownership remains in the main
function.
Filename: src/lib.cairo
#[derive(Drop)]
struct Rectangle {
height: u64,
width: u64,
}
fn main() {
let mut rec = Rectangle { height: 3, width: 10 };
let first_snapshot = @rec; // Take a snapshot of `rec` at this point in time
rec.height = 5; // Mutate `rec` by changing its height
let first_area = calculate_area(first_snapshot); // Calculate the area of the snapshot
let second_area = calculate_area(@rec); // Calculate the current area
println!("The area of the rectangle when the snapshot was taken is {}", first_area);
println!("The current area of the rectangle is {}", second_area);
}
fn calculate_area(rec: @Rectangle) -> u64 {
*rec.height * *rec.width
}
Note: Accessing fields of a snapshot (e.g.,
rec.height
) yields snapshots of those fields, which we desnap with*
to get the values. This works here becauseu64
implementsCopy
. You’ll learn more about desnapping in the next section.
Output dari program ini:
$ scarb cairo-run
warn: `scarb cairo-run` will be deprecated soon
help: use `scarb execute` instead
Compiling no_listing_09_snapshots v0.1.0 (listings/ch04-understanding-ownership/no_listing_09_snapshots/Scarb.toml)
Finished `dev` profile target(s) in 2 seconds
Running no_listing_09_snapshots
The area of the rectangle when the snapshot was taken is 30
The current area of the rectangle is 50
Run completed successfully, returning []
First, notice that all the tuple code in the variable declaration and the function return value is gone. Second, note that we pass @rec
into calculate_area
and, in its definition, we take @Rectangle
rather than Rectangle
.
Mari kita perhatikan lebih dekat panggilan fungsi di sini:
let second_length = calculate_length(@arr1); // Calculate the current length of the array
The @rec
syntax lets us create a snapshot of the value in rec
. Because a snapshot is an immutable view of a value at a specific point in execution, 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.
It’s worth noting that @T
is not a pointer—snapshots are passed by value to functions, just like regular variables. This means that the size of @T
is the same as the size of T
, and when you pass @rec
to calculate_area
, the entire struct (in this case, a Rectangle
with two u64
fields) is copied to the function’s stack. For large data structures, this copying can be avoided by using Box<T>
—provided that there's no need to mutate the value, which we’ll explore in Chapter 12, but for now, understand that snapshots rely on this by-value mechanism.
Demikian pula, tanda fungsi menggunakan @
untuk menunjukkan bahwa tipe dari parameter arr
adalah sebuah snapshot. Mari kita tambahkan beberapa anotasi penjelasan:
fn calculate_area(
rec_snapshot: @Rectangle // rec_snapshot is a snapshot of a Rectangle
) -> u64 {
*rec_snapshot.height * *rec_snapshot.width
} // Here, rec_snapshot goes out of scope and is dropped.
// However, because it is only a view of what the original `rec` contains, the original `rec` can still be used.
The scope in which the variable rec_snapshot
is valid is the same as any function parameter’s scope, but the underlying value of the snapshot is not dropped when rec_snapshot
stops being used. When functions have snapshots as parameters instead of the actual values, we won’t need to return the values in order to give back ownership of the original value, because we never had it.
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 denganmut
.
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.
Unlike snapshots, mutable references allow mutation, but like snapshots, ref
arguments are not pointers—they are also passed by value. When you pass ref rec
, the entire Rectangle
type is copied to the function’s stack, regardless of whether it implements Copy
. This ensures the function operates on its own local version of the data, which is then implicitly returned to the caller. To avoid this copying for large types, Cairo provides the Box<T>
type introduced in Chapter 12 as an alternative, but for this example, the ref
modifier suits our needs perfectly.
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
.