Variabel dan Mutabilitas
Cairo menggunakan model memori yang tidak dapat diubah (immutable), artinya setelah sebuah sel memori ditulisi, sel tersebut tidak dapat ditimpa (overwrite) tapi hanya dapat dibaca. Untuk mencerminkan model memori yang tidak dapat diubah ini, variabel dalam Cairo bersifat tidak dapat diubah secara default. Namun, bahasa ini mengabstraksikan model ini dan memberikan opsi untuk membuat variabel Anda menjadi dapat diubah (mutable). Mari kita jelajahi bagaimana dan mengapa Cairo menerapkan ketidakberubahannya, serta bagaimana Anda dapat membuat variabel Anda menjadi dapat diubah.
Ketika sebuah variabel tidak dapat diubah, setelah nilai terikat pada sebuah nama, Anda tidak dapat mengubah nilai tersebut. Untuk mengilustrasikannya, buatlah proyek baru yang disebut variables dalam direktori cairo_projects Anda dengan menggunakan perintah scarb new variables
.
Kemudian, di direktori baru Anda variables, buka src/lib.cairo dan gantikan kode di dalamnya dengan kode berikut, yang belum dapat dikompilasi:
Filename: src/lib.cairo
fn main() {
let x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
Simpan dan jalankan program menggunakan scarb cairo-run
. Anda seharusnya menerima pesan kesalahan terkait kesalahan ketidakbisaan untuk mengubah variabel, seperti yang ditunjukkan dalam output ini:
$ scarb cairo-run
Compiling no_listing_01_variables_are_immutable v0.1.0 (listings/ch02-common-programming-concepts/no_listing_01_variables_are_immutable/Scarb.toml)
error: Cannot assign to an immutable variable.
--> listings/ch02-common-programming-concepts/no_listing_01_variables_are_immutable/src/lib.cairo:6:5
x = 6;
^***^
error: could not compile `no_listing_01_variables_are_immutable` due to previous error
error: `scarb metadata` exited with error
This example shows how the compiler helps you find errors in your programs. Compiler errors can be frustrating, but they only mean your program isn’t safely doing what you want it to do yet; they do not mean that you’re not a good programmer! Experienced Caironautes still get compiler errors.
Anda menerima pesan kesalahan Cannot assign to an immutable variable.
karena mencoba mengganti nilai kedua pada variabel x
yang bersifat immutable (tidak dapat diubah).
It’s important that we get compile-time errors when we attempt to change a value that’s designated as immutable because this specific situation can lead to bugs. If one part of our code operates on the assumption that a value will never change and another part of our code changes that value, it’s possible that the first part of the code won’t do what it was designed to do. The cause of this kind of bug can be difficult to track down after the fact, especially when the second piece of code changes the value only sometimes.
Cairo, unlike most other languages, has immutable memory. This makes a whole class of bugs impossible, because values will never change unexpectedly. This makes code easier to reason about.
But mutability can be very useful, and can make code more convenient to write. Although variables are immutable by default, you can make them mutable by adding mut
in front of the variable name. Adding mut
also conveys intent to future readers of the code by indicating that other parts of the code will be changing the value associated to this variable.
However, you might be wondering at this point what exactly happens when a variable is declared as mut
, as we previously mentioned that Cairo's memory is immutable. The answer is that the value is immutable, but the variable isn't. The value associated to the variable can be changed. Assigning to a mutable variable in Cairo is essentially equivalent to redeclaring it to refer to another value in another memory cell, but the compiler handles that for you, and the keyword mut
makes it explicit. Upon examining the low-level Cairo Assembly code, it becomes clear that variable mutation is implemented as syntactic sugar, which translates mutation operations into a series of steps equivalent to variable shadowing. The only difference is that at the Cairo level, the variable is not redeclared so its type cannot change.
Sebagai contoh, mari ubah src/lib.cairo menjadi sebagai berikut:
fn main() {
let mut x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
Saat kita menjalankan program sekarang, kita mendapatkan ini:
$ scarb cairo-run
Compiling no_listing_02_adding_mut v0.1.0 (listings/ch02-common-programming-concepts/no_listing_02_adding_mut/Scarb.toml)
Finished `dev` profile target(s) in 4 seconds
Running no_listing_02_adding_mut
The value of x is: 5
The value of x is: 6
Run completed successfully, returning []
Kita diizinkan untuk mengubah nilai yang terikat pada x
dari 5
menjadi 6
ketika mut
digunakan. Pada akhirnya, keputusan untuk menggunakan mutabilitas atau tidak tergantung pada Anda dan tergantung pada apa yang menurut Anda paling jelas dalam situasi tertentu tersebut.
Constant
Seperti variabel yang tidak dapat diubah, Constant adalah nilai yang terikat pada sebuah nama dan tidak diizinkan untuk diubah, namun ada beberapa perbedaan antara konstan dan variabel.
First, you aren’t allowed to use mut
with constants. Constants aren’t just immutable by default—they’re always immutable. You declare constants using the const
keyword instead of the let
keyword, and the type of the value must be annotated. We’ll cover types and type annotations in the next section, “Data Types”, so don’t worry about the details right now. Just know that you must always annotate the type.
Constant variables can be declared with any usual data type, including structs, enums and fixed-size arrays.
Constant hanya dapat dideklarasikan dalam cakupan global, yang membuatnya berguna untuk nilai yang diperlukan oleh banyak bagian kode.
The last difference is that constants may natively be set only to a constant expression, not the result of a value that could only be computed at runtime.
Here’s an example of constants declaration:
struct AnyStruct {
a: u256,
b: u32,
}
enum AnyEnum {
A: felt252,
B: (usize, u256),
}
const ONE_HOUR_IN_SECONDS: u32 = 3600;
const STRUCT_INSTANCE: AnyStruct = AnyStruct { a: 0, b: 1 };
const ENUM_INSTANCE: AnyEnum = AnyEnum::A('any enum');
const BOOL_FIXED_SIZE_ARRAY: [bool; 2] = [true, false];
Nonetheless, it is possible to use the consteval_int!
macro to create a const
variable that is the result of some computation:
const ONE_HOUR_IN_SECONDS: u32 = consteval_int!(60 * 60);
We will dive into more detail about macros in the dedicated section.
Konvensi penamaan konstan dalam Cairo adalah menggunakan huruf kapital semua dengan garis bawah di antara kata-kata.
Konstan valid untuk seluruh waktu program berjalan, dalam cakupan di mana mereka dideklarasikan. Properti ini membuat konstan berguna untuk nilai-nilai dalam domain aplikasi Anda yang mungkin dibutuhkan oleh beberapa bagian dari program, seperti jumlah maksimum poin yang diizinkan oleh pemain dalam permainan, atau kecepatan cahaya.
Memberi nama pada nilai-nilai yang telah diatur secara kaku (hardcoded) yang digunakan di seluruh program Anda sebagai konstan berguna untuk menyampaikan makna dari nilai tersebut kepada pemelihara kode di masa mendatang. Hal ini juga membantu agar hanya ada satu tempat dalam kode Anda yang perlu diubah jika nilai yang diatur secara kaku tersebut perlu diperbarui di masa mendatang.
Shadowing
Shadowing variabel merujuk pada deklarasi variabel baru dengan nama yang sama dengan variabel sebelumnya. Caironautes mengatakan bahwa variabel pertama "tershadow" oleh variabel kedua, yang berarti bahwa variabel kedua yang akan dilihat oleh kompiler saat Anda menggunakan nama variabel tersebut. Pada dasarnya, variabel kedua menggantikan variabel pertama, mengambil penggunaan nama variabel itu sendiri hingga entah itu sendiri yang ter-"shadow" atau cakupan berakhir. Kita dapat "membayangi" (shadow) sebuah variabel dengan menggunakan nama variabel yang sama dan mengulangi penggunaan kata kunci let
seperti berikut:
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("Inner scope x value is: {}", x);
}
println!("Outer scope x value is: {}", x);
}
Program ini pertama mengikat x
pada nilai 5
. Kemudian membuat variabel baru x
dengan mengulangi let x =
, mengambil nilai aslinya dan menambahkan 1
sehingga nilai dari x
kemudian menjadi 6
. Kemudian, dalam sebuah cakupan dalam dengan menggunakan kurung kurawal, pernyataan let
yang ketiga juga melakukan shadowing terhadap x
dan membuat variabel baru, mengalikan nilai sebelumnya dengan 2
sehingga x
memiliki nilai 12
. Ketika cakupan itu selesai, shadowing internal berakhir dan x
kembali menjadi 6
. Ketika program ini dijalankan, akan menghasilkan keluaran berikut:
$ scarb cairo-run
Compiling no_listing_03_shadowing v0.1.0 (listings/ch02-common-programming-concepts/no_listing_03_shadowing/Scarb.toml)
Finished `dev` profile target(s) in 4 seconds
Running no_listing_03_shadowing
Inner scope x value is: 12
Outer scope x value is: 6
Run completed successfully, returning []
Shadowing berbeda dengan menandai variabel sebagai mut
karena kita akan mendapatkan kesalahan pada waktu kompilasi jika secara tidak sengaja mencoba untuk menetapkan kembali (reassign) pada variabel ini tanpa menggunakan kata kunci let
. Dengan menggunakan let
, kita dapat melakukan beberapa transformasi pada sebuah nilai tetapi membuat variabel menjadi tidak dapat diubah setelah transformasi tersebut selesai dilakukan.
Perbedaan lain antara mut
dan shadowing adalah bahwa ketika kita menggunakan kata kunci let
lagi, kita secara efektif membuat variabel baru, yang memungkinkan kita untuk mengubah tipe nilai sambil menggunakan kembali nama yang sama. Seperti yang disebutkan sebelumnya, variabel shadowing dan variabel mutable setara pada tingkat yang lebih rendah. Satu-satunya perbedaan adalah bahwa dengan melakukan shadowing pada sebuah variabel, kompiler tidak akan mengeluh jika Anda mengubah tipe variabel tersebut. Sebagai contoh, katakanlah program kita melakukan konversi tipe antara tipe u64
dan felt252
.
fn main() {
let x: u64 = 2;
println!("The value of x is {} of type u64", x);
let x: felt252 = x.into(); // converts x to a felt, type annotation is required.
println!("The value of x is {} of type felt252", x);
}
Variabel x
pertama memiliki tipe u64
sedangkan variabel x
kedua memiliki tipe felt252
. Dengan demikian, shadowing menghemat kita dari harus menggunakan nama yang berbeda, seperti x_u64
dan x_felt252
; sebaliknya, kita dapat menggunakan kembali nama yang lebih sederhana yaitu x
. Namun, jika kita mencoba menggunakan mut
untuk ini, seperti yang ditunjukkan di sini, kita akan mendapatkan kesalahan pada waktu kompilasi:
fn main() {
let mut x: u64 = 2;
println!("The value of x is: {}", x);
x = 5_u8;
println!("The value of x is: {}", x);
}
Kesalahan tersebut menyatakan bahwa kita mengharapkan tipe u64
(tipe asli) tetapi kita mendapatkan tipe yang berbeda:
$ scarb cairo-run
Compiling no_listing_05_mut_cant_change_type v0.1.0 (listings/ch02-common-programming-concepts/no_listing_05_mut_cant_change_type/Scarb.toml)
error: Unexpected argument type. Expected: "core::integer::u64", found: "core::integer::u8".
--> listings/ch02-common-programming-concepts/no_listing_05_mut_cant_change_type/src/lib.cairo:6:9
x = 5_u8;
^**^
error: could not compile `no_listing_05_mut_cant_change_type` due to previous error
error: `scarb metadata` exited with error
Setelah kita menjelajahi bagaimana variabel bekerja, mari kita lihat lebih banyak jenis data yang bisa mereka miliki.