Bahasa Pemrograman Cairo
oleh Komunitas Cairo dan kontributornya. Terima kasih khusus kepada Starkware melalui OnlyDust, dan Voyager atas dukungan pembuatan buku ini.
Versi teks ini diasumsikan Anda menggunakan Penerjemah Cairo versi 2.2.0. Lihat bagian "Instalasi" dari Bab 1 untuk menginstal atau memperbarui Cairo.
Pengantar
Apa itu Cairo?
Cairo adalah bahasa pemrograman yang dirancang untuk CPU virtual dengan nama yang sama. Aspek unik dari prosesor ini adalah bahwa ia tidak diciptakan untuk batasan fisik dunia kita, tetapi untuk batasan kriptografi, sehingga mampu membuktikan secara efisien eksekusi dari setiap program yang berjalan di atasnya. Ini berarti Anda dapat melakukan operasi yang memakan waktu pada mesin yang tidak Anda percayai, dan memeriksa hasilnya dengan cepat pada mesin yang lebih murah. Sementara Cairo 0 dulunya langsung dikompilasi ke CASM, perakitan CPU Cairo, Cairo 1 adalah bahasa yang lebih tinggi. Cairo 1 pertama-tama dikompilasi ke Sierra, representasi intermediate dari Cairo yang akan dikompilasi nantinya ke subset aman dari CASM. Tujuan dari Sierra adalah memastikan CASM Anda selalu dapat dibuktikan, bahkan ketika komputasi gagal.
Apa yang bisa Anda lakukan dengan itu?
Cairo memungkinkan Anda menghitung nilai-nilai yang dapat dipercaya pada mesin-mesin yang tidak dipercayai. Salah satu penggunaan utama adalah Starknet, solusi untuk skalabilitas Ethereum. Ethereum adalah platform blockchain terdesentralisasi yang memungkinkan pembuatan aplikasi terdesentralisasi di mana setiap interaksi antara pengguna dan d-app diverifikasi oleh semua partisipan. Starknet adalah Layer 2 yang dibangun di atas Ethereum. Alih-alih memiliki semua partisipan jaringan untuk memverifikasi semua interaksi pengguna, hanya satu node, yang disebut prover, yang menjalankan program dan menghasilkan bukti bahwa komputasi dilakukan dengan benar. Bukti-bukti ini kemudian diverifikasi oleh kontrak pintar Ethereum, membutuhkan daya komputasi yang jauh lebih sedikit dibandingkan dengan mengeksekusi interaksi itu sendiri. Pendekatan ini memungkinkan peningkatan throughput dan pengurangan biaya transaksi sambil tetap menjaga keamanan Ethereum.
Apa perbedaannya dengan bahasa pemrograman lain?
Cairo cukup berbeda dari bahasa pemrograman tradisional, terutama dalam hal biaya overhead dan keuntungan utamanya. Program Anda dapat dieksekusi dengan dua cara yang berbeda:
-
Ketika dieksekusi oleh prover, ini mirip dengan bahasa lainnya. Karena Cairo tervirtualisasi, dan karena operasi tidak dirancang secara khusus untuk efisiensi maksimum, ini dapat menyebabkan overhead kinerja tetapi bukan bagian yang paling relevan untuk dioptimalkan.
-
Ketika bukti yang dihasilkan diverifikasi oleh verifier, ini agak berbeda. Ini harus semurah mungkin karena bisa saja diverifikasi pada banyak mesin yang sangat kecil. Untungnya, verifikasi lebih cepat daripada perhitungan dan Cairo memiliki beberapa keunggulan unik untuk meningkatkannya lebih jauh. Salah satunya adalah non-determinisme. Ini adalah topik yang akan Anda bahas lebih detail nanti dalam buku ini, tetapi ideanya adalah bahwa secara teoretis Anda dapat menggunakan algoritma yang berbeda untuk verifikasi daripada untuk perhitungan. Saat ini, menulis kode non-deterministik kustom tidak didukung untuk para pengembang, tetapi pustaka standar memanfaatkan non-determinisme untuk meningkatkan kinerja. Misalnya, mengurutkan sebuah array dalam Cairo memiliki biaya yang sama dengan mengkopi array tersebut. Karena verifier tidak mengurutkan array, hanya memeriksa apakah array tersebut terurut, yang lebih murah.
Aspek lain yang membedakan bahasa ini adalah model memorinya. Dalam Cairo, akses memori bersifat tidak dapat diubah, artinya setelah nilai ditulis ke memori, itu tidak dapat diubah. Cairo 1 menyediakan abstraksi yang membantu pengembang dalam bekerja dengan batasan-batasan ini, tetapi tidak sepenuhnya mensimulasikan mutabilitas. Oleh karena itu, pengembang harus memikirkan dengan hati-hati bagaimana mereka mengelola memori dan struktur data dalam program mereka untuk mengoptimalkan kinerja.
Referensi
- Arsitektur CPU Cairo: https://eprint.iacr.org/2021/1063
- Cairo, Sierra, dan Casm: https://medium.com/nethermind-eth/under-the-hood-of-cairo-1-0-exploring-sierra-7f32808421f5
- Status non-determinism: https://twitter.com/PapiniShahar/status/1638203716535713798
Memulai
Instalasi
Cairo dapat diinstal dengan cara mengunduh Scarb. Scarb mengemas kompiler Cairo dan server bahasa Cairo ke dalam paket yang mudah diinstal sehingga Anda dapat mulai menulis kode Cairo segera.
Scarb juga merupakan pengelola paket Cairo dan sangat terinspirasi oleh Cargo, sistem build dan pengelola paket Rust.
Scarb menangani banyak tugas untuk Anda, seperti membangun kode Anda (baik itu murni Cairo atau kontrak Starknet), mengunduh perpustakaan yang dibutuhkan oleh kode Anda, membangun perpustakaan-perpustakaan tersebut, dan menyediakan dukungan LSP untuk ekstensi VSCode Cairo 1.
Ketika Anda menulis program Cairo yang lebih kompleks, Anda mungkin akan menambahkan dependensi, dan jika Anda memulai proyek menggunakan Scarb, mengelola kode eksternal dan dependensi akan menjadi jauh lebih mudah dilakukan.
Mari mulai dengan menginstal Scarb.
Menginstal Scarb
Persyaratan
Scarb memerlukan Git yang dapat dijalankan dalam variabel lingkungan PATH
.
Instalasi
Untuk menginstal Scarb, silakan merujuk ke petunjuk instalasi. Kami sangat merekomendasikan Anda untuk menginstal Scarb melalui asdf, sebuah alat CLI yang dapat mengelola berbagai versi runtime bahasa pada basis proyek-per-proyek. Hal ini akan memastikan bahwa versi Scarb yang Anda gunakan untuk bekerja pada suatu proyek selalu sesuai dengan yang ditentukan dalam pengaturan proyek, menghindari masalah karena ketidakcocokan versi. Atau, Anda dapat menjalankan perintah berikut di terminal Anda, dan ikuti petunjuk yang muncul di layar. Ini akan menginstal rilis stabil terbaru dari Scarb.
curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh
- Verifikasi instalasi dengan menjalankan perintah berikut di sesi terminal baru, seharusnya mencetak versi Scarb dan Cairo, misalnya:
$ scarb --version
scarb 2.4.0 (cba988e68 2023-12-06)
cairo: 2.4.0 (https://crates.io/crates/cairo-lang-compiler/2.4.0)
sierra: 1.4.0
Menginstal ekstensi VSCode
Cairo memiliki ekstensi VSCode yang menyediakan penyorotan sintaks, penyelesaian kode, dan fitur-fitur berguna lainnya. Anda dapat menginstalnya dari Marketplace VSCode.
Setelah terinstal, masuklah ke pengaturan ekstensi, dan pastikan untuk mencentang opsi Enable Language Server
dan Enable Scarb
.
Hello, World!
Sekarang setelah Anda telah menginstal Cairo melalui Scarb, saatnya untuk menulis program Cairo pertama Anda.
Saat belajar bahasa pemrograman baru, adalah tradisi untuk menulis program kecil yang mencetak teks Hello, world!
ke layar, jadi kita akan melakukan hal yang sama di sini!
Catatan: Buku ini mengasumsikan pemahaman dasar tentang baris perintah. Cairo tidak membuat permintaan spesifik tentang pengeditan atau alat-alat Anda atau di mana kode Anda berada, jadi jika Anda lebih suka menggunakan lingkungan pengembangan terpadu (IDE) daripada baris perintah, silakan gunakan IDE favorit Anda. Tim Cairo telah mengembangkan ekstensi VSCode untuk bahasa Cairo yang dapat Anda gunakan untuk mendapatkan fitur dari server bahasa dan penyorotan kode. Lihat Lampiran D untuk lebih banyak detail.
Membuat Direktori Proyek
Anda akan memulai dengan membuat direktori untuk menyimpan kode Cairo Anda. Tidak masalah bagi Cairo di mana kode Anda berada, tetapi untuk latihan dan proyek dalam buku ini, kami menyarankan untuk membuat direktori cairo_projects di direktori rumah Anda dan menyimpan semua proyek Anda di sana.
Buka terminal dan masukkan perintah berikut untuk membuat direktori cairo_projects dan sebuah direktori untuk proyek "Hello, world!" di dalam direktori cairo_projects.
Catatan: Mulai sekarang, untuk setiap contoh yang ditunjukkan dalam buku ini, kami mengasumsikan bahwa Anda akan bekerja dari direktori proyek Scarb. Jika Anda tidak menggunakan Scarb, dan mencoba menjalankan contoh dari direktori yang berbeda, Anda mungkin perlu menyesuaikan perintahnya atau membuat proyek Scarb.
Untuk Linux, macOS, dan PowerShell di Windows, masukkan ini:
mkdir ~/cairo_projects
cd ~/cairo_projects
Untuk Windows CMD, masukkan ini:
> mkdir "%USERPROFILE%\cairo_projects"
> cd /d "%USERPROFILE%\cairo_projects"
Membuat Proyek dengan Scarb
Mari buat proyek baru menggunakan Scarb.
Beralihlah ke direktori proyek Anda (atau di mana pun Anda memutuskan untuk menyimpan kode Anda). Kemudian jalankan yang berikut:
scarb new hello_world
Ini membuat direktori dan proyek baru yang disebut hello_world
. Kami menamai proyek kami hello_world
, dan Scarb membuat file-filenya di dalam direktori dengan nama yang sama.
Masuklah ke direktori hello_world
dengan perintah cd hello_world
. Anda akan melihat bahwa Scarb telah menghasilkan dua file dan satu direktori untuk kita: file Scarb.toml
dan direktori src dengan file lib.cairo
di dalamnya.
Ini juga menginisialisasi repositori Git baru bersama dengan file .gitignore
Catatan: Git adalah sistem kontrol versi umum. Anda dapat berhenti menggunakan sistem kontrol versi dengan menggunakan opsi
--vcs
flag. Jalankanscarb new -help
untuk melihat opsi yang tersedia.
Buka Scarb.toml di editor teks pilihan Anda. Seharusnya terlihat mirip dengan kode pada Listing 1-2.
Nama File: Scarb.toml
[package]
name = "hello_world"
version = "0.1.0"
edition = "2023_10"
# Lihat lebih banyak kunci dan definisi mereka di https://docs.swmansion.com/scarb/docs/reference/manifest
[dependencies]
# foo = { path = "vendor/foo" }
Listing 1-2: Isi dari Scarb.toml yang dihasilkan oleh scarb new
File ini menggunakan format TOML (Tom's Obvious, Minimal Language), yang merupakan format konfigurasi Scarb.
Baris pertama, [package]
, adalah heading bagian yang menunjukkan bahwa pernyataan-pernyataan berikutnya mengonfigurasi sebuah paket. Ketika kami menambahkan informasi lebih lanjut ke file ini, kami akan menambahkan bagian-bagian lainnya.
Tiga baris berikutnya mengatur informasi konfigurasi yang dibutuhkan Scarb untuk mengompilasi program Anda: nama dan versi Scarb yang digunakan, dan edisi prelude yang digunakan. Prelude adalah kumpulan item yang paling sering digunakan yang secara otomatis diimpor ke setiap program Cairo. Anda dapat mempelajari lebih lanjut tentang prelude di Lampiran E
Baris terakhir, [dependencies]
, adalah awal dari sebuah bagian di mana Anda dapat mencantumkan dependensi-proyek Anda. Dalam Cairo, paket kode disebut sebagai crates. Kami tidak akan membutuhkan crates lain untuk proyek ini.
Catatan: Jika Anda membangun kontrak untuk Starknet, Anda perlu menambahkan dependensi
starknet
sebagaimana disebutkan dalam dokumentasi Scarb.
File lain yang dibuat oleh Scarb adalah src/lib.cairo
, mari kita hapus semua isinya dan masukkan konten berikut, kita akan jelaskan alasannya nanti.
mod hello_world;
Kemudian buat file baru bernama src/hello_world.cairo
dan masukkan kode berikut di dalamnya:
Nama File: src/hello_world.cairo
fn main() {
println!("Hello, World!");
}
Kami baru saja membuat file bernama lib.cairo
, yang berisi deklarasi modul yang merujuk ke modul lain bernama hello_world
, serta file hello_world.cairo
, yang berisi detail implementasi dari modul hello_world
.
Scarb mengharuskan file sumber Anda berada dalam direktori src
.
Direktori proyek tingkat atas disediakan untuk file README, informasi lisensi, file konfigurasi, dan konten lain yang tidak berhubungan dengan kode. Scarb memastikan lokasi tertentu untuk semua komponen proyek, menjaga organisasi yang terstruktur.
Jika Anda memulai proyek yang tidak menggunakan Scarb, Anda dapat
mengonversinya menjadi proyek yang menggunakan Scarb. Pindahkan kode proyek ke dalam direktori src dan buat file Scarb.toml
yang sesuai.
Membangun Proyek Scarb
Dari direktori hello_world
Anda, bangun proyek Anda dengan memasukkan perintah berikut:
$ scarb build
Mengerjakan hello_world v0.1.0 (file:///projects/Scarb.toml)
Menyelesaikan target rilis dalam 0 detik
Perintah ini membuat file sierra
di target/dev
, mari abaikan file sierra
untuk saat ini.
Jika Anda telah menginstal Cairo dengan benar, Anda seharusnya dapat menjalankan dan melihat keluaran berikut:
$ scarb cairo-run --available-gas=200000000
menjalankan hello_world ...
[DEBUG] Hello, World! (raw: 0x48656c6c6f2c20776f726c6421
Pekerjaan selesai dengan sukses, mengembalikan []
Tidak peduli sistem operasi Anda, string Hello, world!
harus tercetak di
terminal.
Jika Hello, world!
tercetak, selamat! Anda telah resmi menulis program Cairo.
Itu membuat Anda seorang pemrogram Cairo—selamat datang!
Anatomi Program Cairo
Mari kita tinjau program "Hello, world!" ini secara detail. Berikut adalah potongan pertama dari teka-teki ini:
fn main() {
}
Baris ini mendefinisikan fungsi bernama main
. Fungsi main
adalah istimewa: ini
selalu adalah kode pertama yang dijalankan dalam setiap program Cairo yang dapat dieksekusi. Di sini, baris pertama mendeklarasikan fungsi bernama main
yang tidak memiliki parameter dan tidak mengembalikan apa pun. Jika ada parameter, mereka akan dimasukkan ke dalam tanda kurung ()
.
Badan fungsi dibungkus dalam {}
. Cairo memerlukan kurung kurawal di sekitar semua
badan fungsi. Adalah gaya yang baik untuk menempatkan kurung kurawal pembuka di baris yang sama dengan deklarasi fungsi, menambahkan satu spasi di antaranya.
Catatan: Jika Anda ingin tetap menggunakan gaya standar di seluruh proyek Cairo, Anda dapat menggunakan alat pemformat otomatis yang tersedia dengan
scarb fmt
untuk memformat kode Anda dalam gaya tertentu (lebih lanjut tentangscarb fmt
di Lampiran D). Tim Cairo telah menyertakan alat ini dengan distribusi standar Cairo, seperticairo-run
, jadi seharusnya sudah terinstal di komputer Anda!
Badan fungsi main
memiliki kode berikut:
println!("Hello, World!");
Baris ini melakukan seluruh pekerjaan dalam program kecil ini: mencetak teks ke layar. Ada empat detail penting yang perlu diperhatikan di sini.
Pertama, gaya Cairo adalah dengan indentasi empat spasi, bukan tab.
Kedua, println!
memanggil sebuah makro Cairo. Jika ini memanggil sebuah fungsi, itu akan dimasukkan sebagai println
(tanpa !
). Kami akan membahas makro Cairo dengan lebih detail di Bab Makro. Untuk saat ini, yang perlu Anda ketahui adalah penggunaan !
berarti Anda memanggil sebuah makro bukan fungsi normal dan bahwa makro tidak selalu mengikuti aturan yang sama dengan fungsi.
Ketiga, Anda melihat string "Hello, world!"
. Kami meneruskan string ini sebagai argumen ke println!
, dan string tersebut dicetak ke layar.
Keempat, kami mengakhiri baris dengan titik koma (;
), yang menunjukkan bahwa ini
ekspresi telah selesai dan yang berikutnya siap dimulai. Sebagian besar baris kode Cairo
diakhiri dengan titik koma.
Menjalankan Tes
Untuk menjalankan semua tes yang terkait dengan paket tertentu, Anda dapat menggunakan perintah scarb test
.
Ini bukanlah runner tes itu sendiri, melainkan mendelegasikan pekerjaan ke solusi pengujian yang dipilih. Scarb dilengkapi dengan ekstensi scarb cairo-test
yang telah terinstal sebelumnya, yang menggabungkan runner tes asli Cairo. Ini adalah runner tes default yang digunakan oleh scarb test.
Untuk menggunakan runner tes pihak ketiga, silakan lihat dokumentasi Scarb.
Fungsi-fungsi tes ditandai dengan atribut #[test]
, dan menjalankan scarb test
akan menjalankan semua fungsi tes dalam basis kode Anda di bawah direktori src/
.
├── Scarb.toml
├── src
│ ├── lib.cairo
│ └── file.cairo
Struktur proyek Scarb contoh
Mari kita ringkas apa yang telah kita pelajari tentang Scarb sejauh ini:
- Kita dapat membuat proyek menggunakan
scarb new
. - Kita dapat membangun proyek menggunakan
scarb build
untuk menghasilkan kode Sierra yang sudah dikompilasi. - Kita dapat mendefinisikan skrip kustom di
Scarb.toml
dan memanggilnya dengan perintahscarb run
. - Kita dapat menjalankan tes menggunakan perintah
scarb test
.
Keuntungan tambahan dari menggunakan Scarb adalah bahwa perintahnya sama tidak peduli pada sistem operasi mana Anda bekerja. Jadi, pada titik ini, kami tidak akan lagi memberikan instruksi spesifik untuk Linux dan macOS versus Windows.
Ringkasan
Anda telah memulai perjalanan Cairo Anda dengan sangat baik! Di bab ini, Anda telah belajar bagaimana:
- Menginstal versi stabil terbaru dari Cairo
- Menulis dan menjalankan program "Hello, Scarb!" menggunakan
scarb
langsung - Membuat dan menjalankan proyek baru menggunakan konvensi Scarb
- Melakukan tes menggunakan perintah
scarb test
Ini adalah waktu yang tepat untuk membangun program yang lebih besar untuk terbiasa membaca dan menulis kode Cairo.
Konsep-Konsep Pemrograman Umum
Bab ini membahas konsep-konsep yang muncul dalam hampir setiap bahasa pemrograman dan bagaimana cara kerjanya di Cairo. Banyak bahasa pemrograman memiliki kesamaan pada inti mereka. Tidak ada konsep yang disajikan dalam bab ini yang unik bagi Cairo, namun kita akan membahasnya dalam konteks Cairo dan menjelaskan konvensi seputar penggunaan konsep-konsep ini.
Secara khusus, Anda akan mempelajari tentang variabel, tipe-tipe dasar, fungsi, komentar, dan aliran kontrol. Dasar-dasar ini akan ada dalam setiap program Cairo, dan mempelajarinya sejak awal akan memberikan fondasi yang kuat untuk memulai.
Variabel dan Mutabilitas
Cairo menggunakan model memori yang tidak dapat diubah (immutable), yang berarti bahwa setelah sebuah sel memori ditulis, sel tersebut tidak dapat ditimpa (overwrite) tetapi hanya dapat dibaca. Untuk mencerminkan model memori yang tidak dapat diubah ini, variabel dalam Cairo secara default tidak dapat diubah (immutable). Namun, bahasa ini menyederhanakan model ini dan memberikan opsi untuk membuat variabel Anda dapat diubah (mutable). Mari kita jelajahi bagaimana dan mengapa Cairo menerapkan ketidakmutabilan (immutability), dan bagaimana Anda dapat membuat variabel Anda menjadi mutable.
Ketika sebuah variabel tidak dapat diubah, setelah sebuah variabel terikat pada sebuah nilai, Anda tidak dapat mengubah variabel tersebut. Untuk mengilustrasikannya, buat proyek baru bernama variables dalam direktori cairo_projects Anda dengan menggunakan scarb new variables
.
Kemudian, di direktori variables baru Anda, buka src/lib.cairo yang baru dan gantikan kode di dalamnya dengan kode berikut, yang belum akan berhasil dikompilasi:
{{#include ../listings/ch02-common-programming-concepts/no_listing_01_variables_are_immutable/src/lib.cairo}}
Simpan dan jalankan program menggunakan scarb cairo-run --available-gas=200000000
. Anda seharusnya menerima pesan kesalahan yang berkaitan dengan kesalahan ketidakmutabilan, seperti yang ditunjukkan dalam keluaran berikut:
error: Cannot assign to an immutable variable.
--> lib.cairo:5:5
x = 6;
^***^
Error: failed to compile: src/lib.cairo
Contoh ini menunjukkan bagaimana kompiler membantu Anda menemukan kesalahan dalam program Anda. Kesalahan kompiler dapat menjengkelkan, tetapi sebenarnya itu hanya berarti bahwa program Anda belum melakukan dengan aman apa yang ingin Anda lakukan; itu bukan berarti bahwa Anda bukan seorang programmer yang baik! Para Caironautes berpengalaman tetap mendapatkan kesalahan kompiler.
Anda menerima pesan kesalahan Cannot assign to an immutable variable.
karena Anda mencoba untuk memberikan nilai kedua ke variabel x
yang tidak dapat diubah.
Penting bagi kita untuk mendapatkan kesalahan pada waktu kompilasi ketika kita mencoba untuk mengubah nilai yang ditetapkan sebagai tidak dapat diubah karena situasi spesifik ini dapat menyebabkan bug. Jika salah satu bagian kode kita beroperasi dengan asumsi bahwa sebuah nilai tidak akan pernah berubah dan bagian lain dari kode kita mengubah nilai tersebut, mungkin bagian pertama kode tersebut tidak akan melakukan apa yang dirancang untuk dilakukan. Penyebab bug semacam ini dapat sulit dilacak setelahnya, terutama ketika bagian kedua kode tersebut hanya mengubah nilai itu kadang-kadang.
Cairo, berbeda dengan kebanyakan bahasa lain, memiliki memori yang tidak dapat diubah. Hal ini membuat sejumlah bug menjadi tidak mungkin, karena nilai tidak akan berubah secara tak terduga. Hal ini membuat kode menjadi lebih mudah untuk dianalisis.
Namun, mutabilitas dapat sangat berguna, dan dapat membuat kode menjadi lebih nyaman untuk ditulis. Meskipun variabel secara default tidak dapat diubah, Anda dapat membuatnya menjadi mutable dengan menambahkan mut
di depan nama variabel. Menambahkan mut
juga menyampaikan niat kepada pembaca kode di masa depan dengan menunjukkan bahwa bagian lain dari kode akan mengubah nilai yang terkait dengan variabel ini.
Namun, pada titik ini mungkin Anda bertanya-tanya apa yang sebenarnya terjadi ketika sebuah variabel dinyatakan sebagai mut
, karena sebelumnya kami menyebutkan bahwa memori Cairo tidak dapat diubah. Jawabannya adalah nilai tidak dapat diubah, tetapi variabel-nya dapat. Nilai yang ditunjuk oleh variabel dapat diubah. Menugaskan ke variabel yang mutable dalam Cairo pada dasarnya setara dengan mendeklarasikan ulang variabel untuk merujuk pada nilai lain di sel memori lain, tetapi kompiler menangani hal tersebut untuk Anda, dan kata kunci mut
membuatnya eksplisit. Setelah memeriksa kode Assembly Cairo tingkat rendah, menjadi jelas bahwa mutasi variabel diimplementasikan sebagai gula sintaksis, yang menerjemahkan operasi mutasi menjadi serangkaian langkah yang setara dengan variable shadowing. Satu-satunya perbedaan adalah bahwa pada level Cairo, variabel tidak dideklarasikan ulang sehingga tipe variabel tidak dapat berubah.
Sebagai contoh, mari ubah src/lib.cairo menjadi seperti berikut:
Nama file: src/lib.cairo
{{#include ../listings/ch02-common-programming-concepts/no_listing_02_adding_mut/src/lib.cairo}}
Ketika kita menjalankan program sekarang, kita akan mendapatkan keluaran ini:
$ scarb cairo-run --available-gas=200000000
[DEBUG] (raw: 5)
[DEBUG] (raw: 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.
Konstan
Seperti variabel yang tidak dapat diubah, konstan adalah nilai yang terikat pada sebuah nama dan tidak diizinkan untuk berubah, tetapi ada beberapa perbedaan antara konstan dan variabel.
Pertama, Anda tidak diizinkan untuk menggunakan mut
dengan konstan. Konstan tidak hanya tidak dapat diubah secara default—mereka selalu tidak dapat diubah. Anda mendeklarasikan konstan menggunakan kata kunci const
alih-alih kata kunci let
, dan tipe nilai harus diannotasikan. Kita akan membahas tentang tipe dan annotasi tipe pada bagian selanjutnya, “Tipe Data”, jadi jangan khawatir tentang detail-detailnya sekarang. Yang penting, Anda harus selalu menambahkan annotasi tipe.
Konstan hanya dapat dideklarasikan pada scope global, yang membuatnya berguna untuk nilai-nilai yang banyak bagian kode perlu mengetahuinya.
Perbedaan terakhir adalah bahwa konstan hanya dapat diatur ke ekspresi konstan, bukan hasil dari nilai yang hanya dapat dihitung saat runtime. Hanya konstan literal yang didukung saat ini.
Berikut contoh deklarasi konstan:
const SATU_JAM_DALAM_DETIK: u32 = 3600;
Konvensi penamaan Cairo untuk konstan adalah dengan menggunakan huruf besar semua dengan garis bawah di antara kata-kata.
Konstan valid selama program berjalan, dalam scope di mana mereka dideklarasikan. Properti ini membuat konstan berguna untuk nilai-nilai dalam domain aplikasi Anda yang mungkin dibutuhkan oleh banyak bagian program, seperti jumlah maksimum poin yang dapat diperoleh oleh pemain dalam sebuah permainan, atau kecepatan cahaya.
Memberi nama nilai-nilai yang dikodekan secara kaku yang digunakan di seluruh program Anda sebagai konstan berguna dalam menyampaikan makna dari nilai tersebut kepada para pemelihara kode di masa depan. Ini juga membantu jika hanya ada satu tempat dalam kode Anda yang perlu Anda ubah jika nilai yang dikodekan secara kaku tersebut perlu diperbarui di masa depan.
Shadowing
Variable shadowing mengacu pada deklarasi variabel baru dengan nama yang sama seperti variabel sebelumnya. Caironautes mengatakan bahwa variabel pertama disandingkan (shadowed) oleh yang kedua, yang berarti bahwa variabel kedua yang akan dilihat oleh kompiler ketika Anda menggunakan nama variabel tersebut. Pada dasarnya, variabel kedua menutupi (overshadow) yang pertama, mengambil penggunaan nama variabel itu sendiri sampai entah itu sendiri yang disandingkan atau scope berakhir. Kita dapat menutupi variabel dengan menggunakan nama variabel yang sama dan mengulangi penggunaan kata kunci let
sebagai berikut:
Nama file: src/lib.cairo
{{#include ../listings/ch02-common-programming-concepts/no_listing_03_shadowing/src/lib.cairo}}
Program ini pertama kali mengikat x
pada nilai 5
. Kemudian, ia membuat variabel baru x
dengan mengulangi let x =
, mengambil nilai asli dan menambahkan 1
sehingga nilai x
kemudian menjadi 6
. Kemudian, dalam scope dalam dengan menggunakan kurung kurawal, pernyataan let
ketiga juga menutupi x
dan membuat variabel baru, mengalikan nilai sebelumnya dengan 2
sehingga memberikan nilai x
menjadi 12
. Ketika scope itu selesai, penutupan dalam berakhir dan x
kembali menjadi 6
. Ketika kita menjalankan program ini, akan mengeluarkan keluaran berikut:
scarb cairo-run --available-gas=200000000
[DEBUG] Nilai x pada scope dalam adalah: (raw: 7033328135641142205392067879065573688897582790068499258)
[DEBUG]
(raw: 12)
[DEBUG] Nilai x pada scope luar adalah: (raw: 7610641743409771490723378239576163509623951327599620922)
[DEBUG] (raw: 6)
Run completed successfully, returning []
Shadowing berbeda dengan menandai variabel sebagai mut
karena kita akan mendapatkan kesalahan waktu kompilasi jika secara tidak sengaja kita mencoba untuk melakukan penugasan ulang pada variabel ini tanpa menggunakan kata kunci let
. Dengan menggunakan let
, kita dapat melakukan beberapa transformasi pada sebuah nilai tetapi variabelnya tetap tidak dapat diubah setelah transformasi tersebut selesai.
Perbedaan lain antara mut
dan shadowing adalah bahwa ketika kita menggunakan kata kunci let
lagi, kita pada dasarnya membuat variabel baru, yang memungkinkan kita untuk mengubah tipe nilai sambil menggunakan kembali nama yang sama. Seperti yang disebutkan sebelumnya, variabel shadowing dan variabel yang dapat diubah (mutable variables) setara pada level yang lebih rendah.
Perbedaan utamanya adalah bahwa dengan menutupi (shadowing) sebuah variabel, kompiler tidak akan mengeluh jika Anda mengubah tipe variabel tersebut. Sebagai contoh, katakanlah program kami melakukan konversi tipe antara tipe u64
dan felt252
.
{{#include ../listings/ch02-common-programming-concepts/no_listing_04_shadowing_different_type/src/lib.cairo}}
Variabel x
pertama memiliki tipe u64
sementara variabel x
kedua memiliki tipe felt252
. Shadowing ini menghemat kita dari harus membuat 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 waktu kompilasi:
{{#include ../listings/ch02-common-programming-concepts/no_listing_05_mut_cant_change_type/src/lib.cairo}}
Kesalahan tersebut mengatakan kita mengharapkan sebuah u64
(tipe asli) tetapi kita mendapatkan tipe yang berbeda:
$ scarb cairo-run --available-gas=200000000
error: Unexpected argument type. Expected: "core::integer::u64", found: "core::felt252".
--> lib.cairo:9:9
x = 100_felt252;
^*********^
Error: failed to compile: src/lib.cairo
Sekarang setelah kita telah menjelajahi bagaimana variabel bekerja, mari kita lihat lebih banyak jenis data yang dapat mereka miliki.
Jenis Data
Setiap nilai dalam Cairo memiliki jenis data tertentu, yang memberi tahu Cairo jenis data apa yang di spesifikasikan sehingga Cairo tahu bagaimana cara bekerja dengan data tersebut. Bagian ini mencakup dua subset dari jenis data: skalar dan gabungan.
Ingatlah bahwa Cairo adalah bahasa berjenis statis, yang berarti bahwa ia harus mengetahui jenis dari semua variabel pada waktu kompilasi. Compiler biasanya dapat menyimpulkan jenis yang diinginkan berdasarkan nilai dan penggunaannya. Dalam kasus di mana banyak jenis mungkin, kita dapat menggunakan metode cast di mana kita menentukan jenis keluaran yang diinginkan.
{{#include ../listings/ch02-common-programming-concepts/no_listing_06_data_types/src/lib.cairo}}
Anda akan melihat anotasi tipe yang berbeda untuk jenis data lainnya.
Jenis Scalar
Jenis scalar mewakili sebuah nilai tunggal. Cairo memiliki tiga jenis skalar utama: felts, integers, dan booleans. Anda mungkin mengenali ini dari bahasa pemrograman lain. Mari kita bahas bagaimana cara kerjanya di Cairo.
Jenis Felt
Di Cairo, jika Anda tidak menentukan jenis variabel atau argumen, tipe defaultnya adalah elemen lapangan, yang direpresentasikan oleh kata kunci felt252
. Dalam konteks Cairo, ketika kita mengatakan "sebuah elemen lapangan", kita maksudkan sebuah bilangan bulat dalam rentang 0 <= x < P
, di mana P
adalah bilangan prima yang sangat besar saat ini sama dengan P = 2^{251} + 17 * 2^{192}+1
. Ketika menambah, mengurangi, atau mengalikan, jika hasilnya jatuh di luar rentang yang ditentukan oleh bilangan prima, maka akan terjadi overflow, dan kelipatan P yang sesuai akan ditambahkan atau dikurangkan untuk membawa hasil kembali ke dalam rentang (artinya, hasilnya dihitung modulo P).
Perbedaan terpenting antara bilangan bulat dan elemen lapangan adalah pada pembagian: Pembagian elemen lapangan (dan oleh karena itu pembagian di Cairo) tidak seperti pembagian CPU reguler, di mana pembagian bilangan bulat x / y
didefinisikan sebagai [x/y]
di mana bagian bilangan bulat dari hasil bagi dikembalikan (sehingga Anda mendapatkan 7 / 3 = 2
) dan mungkin atau mungkin tidak memenuhi persamaan (x / y) * y == x
, tergantung pada kelipatan x
oleh y
.
Di Cairo, hasil dari x/y
didefinisikan untuk selalu memenuhi persamaan (x / y) * y == x
. Jika y membagi x sebagai bilangan bulat, Anda akan mendapatkan hasil yang diharapkan di Cairo (misalnya 6 / 2
akan menghasilkan 3
). Tetapi ketika y tidak membagi x, Anda mungkin mendapatkan hasil yang mengejutkan: Sebagai contoh, karena 2 * ((P+1)/2) = P+1 ≡ 1 mod[P]
, nilai dari 1 / 2
di Cairo adalah (P+1)/2
(dan bukan 0 atau 0.5), karena memenuhi persamaan di atas.
Jenis Integer
Jenis felt252 adalah jenis fundamental yang berfungsi sebagai dasar untuk membuat semua jenis dalam pustaka inti.
Namun, sangat disarankan bagi para pemrogram untuk menggunakan jenis integer alih-alih jenis felt252
, karena jenis integer dilengkapi dengan fitur keamanan tambahan yang memberikan perlindungan ekstra terhadap kerentanan potensial dalam kode, seperti pemeriksaan overflow. Dengan menggunakan jenis integer ini, para pemrogram dapat memastikan bahwa program-program mereka lebih aman dan kurang rentan terhadap serangan atau ancaman keamanan lainnya.
Sebuah integer adalah sebuah bilangan tanpa komponen pecahan. Deklarasi jenis ini menunjukkan jumlah bit yang dapat digunakan pemrogram untuk menyimpan integer tersebut.
Tabel 3-1 menunjukkan
jenis integer bawaan dalam Cairo. Kita dapat menggunakan salah satu variasi ini untuk mendeklarasikan
jenis dari nilai integer.
Tabel 3-1: Jenis Integer dalam Cairo
Panjang | Tidak Bertanda |
---|---|
8-bit | u8 |
16-bit | u16 |
32-bit | u32 |
64-bit | u64 |
128-bit | u128 |
256-bit | u256 |
32-bit | usize |
Setiap variasi memiliki ukuran eksplisit. Perlu diperhatikan bahwa untuk saat ini, jenis usize
hanya merupakan alias untuk u32
; namun, ini mungkin berguna ketika pada masa depan Cairo dapat dikompilasi ke MLIR.
Karena variabel bersifat tidak bertanda, mereka tidak dapat menyimpan angka negatif. Kode berikut akan menyebabkan program panic:
{{#include ../listings/ch02-common-programming-concepts/no_listing_07_integer_types/src/lib.cairo}}
Semua jenis integer yang disebutkan sebelumnya cocok ke dalam felt252
, kecuali untuk u256
yang memerlukan 4 bit tambahan untuk disimpan. Di balik layar, u256
pada dasarnya adalah sebuah struktur dengan 2 bidang: u256 {low: u128, high: u128}
Anda dapat menulis literal integer dalam bentuk apapun yang ditunjukkan dalam Tabel 3-2. Perlu
dicatat bahwa literal angka yang dapat menjadi beberapa jenis numerik memungkinkan penambahan tipe,
seperti 57_u8
, untuk menunjukkan tipe.
Tabel 3-2: Literal Integer dalam Cairo
Literal numerik | Contoh |
---|---|
Desimal | 98222 |
Heksadesimal | 0xff |
Oktales | 0o04321 |
Biner | 0b01 |
Jadi bagaimana Anda tahu jenis integer mana yang akan digunakan? Cobalah untuk memperkirakan nilai maksimum yang dapat dipegang oleh integer Anda dan pilih ukuran yang tepat.
Situasi utama di mana Anda akan menggunakan usize
adalah saat melakukan pengindeksan pada koleksi data tertentu.
Operasi Numerik
Cairo mendukung operasi matematika dasar yang diharapkan untuk semua jenis integer:
penambahan, pengurangan, perkalian, pembagian, dan sisa bagi. Pembagian integer memotong ke nol ke bilangan bulat terdekat. Kode berikut menunjukkan
bagaimana cara menggunakan setiap operasi numerik dalam sebuah pernyataan let
:
{{#include ../listings/ch02-common-programming-concepts/no_listing_08_numeric_operations/src/lib.cairo}}
Setiap ekspresi dalam pernyataan ini menggunakan operator matematika dan mengevaluasi menjadi sebuah nilai tunggal, yang kemudian diikat ke sebuah variabel.
Lampiran B berisi daftar semua operator yang disediakan oleh Cairo.
Jenis Boolean
Seperti dalam kebanyakan bahasa pemrograman lainnya, tipe data Boolean dalam Cairo memiliki dua nilai yang mungkin: true
dan false
. Boolean memiliki ukuran satu felt252. Tipe data Boolean dalam
Cairo ditentukan menggunakan bool
. Sebagai contoh:
{{#include ../listings/ch02-common-programming-concepts/no_listing_09_boolean_type/src/lib.cairo}}
Cara utama menggunakan nilai Boolean adalah melalui kondisional, seperti ekspresi if
. Kami akan menutup bagaimana ekspresi if
bekerja di Cairo pada bagian [“Aliran Kontrol”][control-flow].
Jenis String Pendek
Cairo tidak memiliki jenis data asli untuk string, tetapi Anda dapat menyimpan karakter yang membentuk apa yang kami sebut sebagai "string pendek" di dalam felt252
. Sebuah string pendek memiliki panjang maksimum 31 karakter. Hal ini dilakukan untuk memastikan bahwa dapat muat dalam sebuah felt tunggal (sebuah felt adalah 252 bit, satu karakter ASCII adalah 8 bit).
Berikut adalah beberapa contoh deklarasi nilai dengan meletakkannya di antara tanda kutip satu:
{{#rustdoc_include ../listings/ch02-common-programming-concepts/no_listing_10_short_string_type/src/lib.cairo:2:3}}
Jenis Casting
Di Cairo, Anda dapat mengonversi jenis data skalar dari satu jenis ke jenis lainnya dengan menggunakan metode try_into
dan into
yang disediakan oleh trait TryInto
dan Into
dari pustaka inti.
Metode try_into
memungkinkan pencast tipe yang aman ketika tipe tujuan mungkin tidak cocok dengan nilai sumber. Perlu diingat bahwa try_into
mengembalikan tipe Option<T>
, yang perlu Anda bungkus (unwrap) untuk mengakses nilai baru.
Di sisi lain, metode into
dapat digunakan untuk pencast tipe ketika kesuksesan dijamin, seperti saat tipe sumber lebih kecil dari tipe tujuan.
Untuk melakukan konversi, panggil var.into()
atau var.try_into()
pada nilai sumber untuk mencastingnya ke tipe lain. Tipe variabel baru harus didefinisikan secara eksplisit, seperti yang ditunjukkan dalam contoh di bawah ini.
{{#include ../listings/ch02-common-programming-concepts/no_listing_11_type_casting/src/lib.cairo}}
Jenis Tuple
Sebuah tuple adalah cara umum untuk mengelompokkan sejumlah nilai dengan berbagai jenis ke dalam satu jenis gabungan. Tuple memiliki panjang tetap: setelah dideklarasikan, mereka tidak dapat tumbuh atau menyusut dalam ukuran.
Kita membuat tuple dengan menuliskan daftar nilai yang dipisahkan koma di dalam tanda kurung. Setiap posisi dalam tuple memiliki tipe, dan tipe-tipe nilai yang berbeda di dalam tuple tidak harus sama. Kami menambahkan anotasi tipe opsional dalam contoh ini:
{{#include ../listings/ch02-common-programming-concepts/no_listing_12_tuple_type/src/lib.cairo}}
Variabel tup
terikat ke seluruh tuple karena tuple dianggap sebagai
elemen tunggal. Untuk mendapatkan nilai-nilai individu dari sebuah tuple, kita dapat
menggunakan pola pencocokan untuk mendestruksi nilai tuple, seperti ini:
{{#include ../listings/ch02-common-programming-concepts/no_listing_13_tuple_destructuration/src/lib.cairo}}
Program ini pertama-tama membuat sebuah tuple dan mengikatnya ke variabel tup
. Kemudian
menggunakan pola dengan let
untuk mengambil tup
dan mengubahnya menjadi tiga variabel terpisah, x
, y
, dan z
. Ini disebut destructuring karena memecah
tuple tunggal menjadi tiga bagian. Akhirnya, program mencetak y adalah enam
karena nilai dari
y
adalah 6
.
Kita juga dapat mendeklarasikan tuple dengan nilai dan tipe pada saat yang sama. Sebagai contoh:
{{#include ../listings/ch02-common-programming-concepts/no_listing_14_tuple_types/src/lib.cairo}}
Jenis unit ()
Sebuah jenis unit adalah jenis yang hanya memiliki satu nilai ()
.
Ini direpresentasikan oleh sebuah tuple tanpa elemen.
Ukurannya selalu nol, dan dijamin tidak ada dalam kode yang dikompilasi.
Fungsi
Fungsi sering digunakan dalam kode Cairo. Anda sudah melihat salah satu fungsi paling penting dalam bahasa ini: fungsi main
, yang merupakan titik masuk banyak program. Anda juga sudah melihat kata kunci fn
, yang memungkinkan Anda mendeklarasikan fungsi baru.
Kode Cairo menggunakan snake case sebagai gaya konvensional untuk nama fungsi dan variabel, di mana semua huruf huruf kecil dan garis bawah memisahkan kata-kata. Berikut adalah contoh program yang berisi definisi fungsi:
{{#include ../listings/ch02-common-programming-concepts/no_listing_15_functions/src/lib.cairo}}
Kami mendefinisikan fungsi di Cairo dengan mengetikkan fn
diikuti oleh nama fungsi dan sepasang tanda kurung. Kurung kurawal memberi tahu kompiler di mana tubuh fungsi dimulai dan berakhir.
Kami dapat memanggil setiap fungsi yang telah kami definisikan dengan mengetikkan namanya diikuti oleh sepasang tanda kurung. Karena another_function
didefinisikan dalam program, itu dapat dipanggil dari dalam fungsi main
. Perhatikan bahwa kami mendefinisikan another_function
sebelum fungsi main
dalam kode sumber; kami juga bisa mendefinisikannya setelahnya. Cairo tidak peduli di mana Anda mendefinisikan fungsi Anda, asalkan mereka didefinisikan di dalam cakupan yang dapat dilihat oleh pemanggil.
Mari mulai proyek baru dengan Scarb yang diberi nama functions untuk menjelajahi fungsi lebih lanjut. Tempatkan contoh another_function
di src/lib.cairo dan jalankannya. Anda seharusnya melihat keluaran berikut:
$ scarb cairo-run --available-gas=200000000
[DEBUG] Hello, world! (raw: 5735816763073854953388147237921)
[DEBUG] Another function. (raw: 22265147635379277118623944509513687592494)
Baris-baris dieksekusi sesuai dengan urutan mereka muncul dalam fungsi main
. Pertama-tama pesan "Hello, world!" mencetak, dan kemudian another_function
dipanggil dan pesannya dicetak.
Parameter
Kita dapat mendefinisikan fungsi agar memiliki parameter, yaitu variabel khusus yang merupakan bagian dari tanda tangan fungsi. Ketika sebuah fungsi memiliki parameter, Anda dapat memberikan nilai konkret untuk parameter tersebut. Secara teknis, nilai konkret disebut argumen, tetapi dalam percakapan santai, orang cenderung menggunakan kata parameter dan argumen secara bergantian untuk variabel dalam definisi fungsi atau nilai konkret yang dilewatkan saat Anda memanggil fungsi.
Pada versi ini dari another_function
, kita menambahkan satu parameter:
{{#include ../listings/ch02-common-programming-concepts/no_listing_16_single_param/src/lib.cairo}}
Coba jalankan program ini; Anda seharusnya mendapatkan keluaran berikut:
$ scarb cairo-run --available-gas=200000000
x = 5
Run completed successfully, returning []
Remaining gas: 1999829300
Deklarasi another_function
memiliki satu parameter bernama x
. Tipe x
ditentukan sebagai felt252
. Ketika kami memasukkan 5
ke another_function
, makro println!
menempatkan 5
di tempat sepasang kurung kurawal yang berisi x
di dalam string format.
Dalam tanda tangan fungsi, Anda harus mendeklarasikan tipe setiap parameter. Ini adalah keputusan yang disengaja dalam desain Cairo: mensyaratkan anotasi tipe dalam definisi fungsi berarti kompiler hampir tidak pernah perlu Anda menggunakannya di tempat lain dalam kode untuk mengetahui tipe apa yang Anda maksudkan. Kompiler juga dapat memberikan pesan kesalahan yang lebih membantu jika ia tahu tipe apa yang diharapkan oleh fungsi.
Saat mendefinisikan beberapa parameter, pisahkan deklarasi parameter dengan koma, seperti ini:
{{#include ../listings/ch02-common-programming-concepts/no_listing_17_multiple_params/src/lib.cairo}}
Contoh ini membuat fungsi bernama print_labeled_measurement
dengan dua parameter. Parameter pertama dinamai value
dan bertipe u128
. Parameter kedua dinamai unit_label
dan bertipe ByteArray
- tipe internal Cairo untuk mewakili literal string. Fungsi kemudian mencetak teks yang berisi baik value
maupun unit_label
.
Mari coba jalankan kode ini. Gantilah program yang saat ini ada di file src/lib.cairo proyek functions Anda dengan contoh sebelumnya dan jalankan dengan scarb cairo-run --available-gas=200000000
:
$ scarb cairo-run --available-gas=200000000
The measurement is: 5h
Run completed successfully, returning []
Remaining gas: 1999755400
Karena kami memanggil fungsi dengan 5
sebagai nilai untuk value
dan "h"
sebagai nilai untuk unit_label
, keluaran program berisi nilai-nilai tersebut.
Parameter bernama
Dalam Cairo, parameter bernama memungkinkan Anda menentukan nama argumen saat Anda memanggil fungsi. Hal ini membuat pemanggilan fungsi lebih mudah dibaca dan deskriptif. Jika Anda ingin menggunakan parameter bernama, Anda perlu menentukan nama parameter dan nilai yang ingin Anda lewatkan. Sintaksnya adalah nama_parameter: nilai
. Jika Anda melewati variabel yang memiliki nama yang sama dengan parameter, Anda dapat menulis :nama_parameter
daripada nama_parameter: nama_variabel
.
Berikut adalah contoh:
{{#include ../listings/ch02-common-programming-concepts/no_listing_30_named_parameters/src/lib.cairo}}
Pernyataan dan Ekspresi
Tubuh fungsi terdiri dari serangkaian pernyataan yang opsionalnya diakhiri oleh sebuah ekspresi. Sejauh ini, fungsi yang telah kita bahas belum mencakup ekspresi penutup, tetapi Anda telah melihat sebuah ekspresi sebagai bagian dari sebuah pernyataan. Karena Cairo adalah bahasa berbasis ekspresi, ini adalah perbedaan penting untuk dipahami. Bahasa lain tidak memiliki perbedaan yang sama, jadi mari kita lihat apa itu pernyataan dan ekspresi dan bagaimana perbedaan mereka mempengaruhi tubuh fungsi.
- Pernyataan adalah instruksi yang melakukan suatu tindakan dan tidak mengembalikan nilai.
- Ekspresi mengevaluasi menjadi nilai hasil. Mari kita lihat beberapa contoh.
Sebenarnya, kita sudah menggunakan pernyataan dan ekspresi. Membuat variabel dan memberikan nilai kepadanya dengan kata kunci let
adalah suatu pernyataan. Dalam Listing 2-1, let y = 6;
adalah sebuah pernyataan.
{{#include ../listings/ch02-common-programming-concepts/listing_01_statement/src/lib.cairo}}
Listing 2-1: Deklarasi fungsi main
yang berisi satu pernyataan
Definisi fungsi juga merupakan pernyataan; seluruh contoh sebelumnya adalah pernyataan itu sendiri.
Pernyataan tidak mengembalikan nilai. Oleh karena itu, Anda tidak dapat menugaskan pernyataan let
ke variabel lain, seperti yang dicoba oleh kode berikut; Anda akan mendapatkan kesalahan:
{{#include ../listings/ch02-common-programming-concepts/no_listing_18_statements_dont_return_values/src/lib.cairo}}
Ketika Anda menjalankan program ini, kesalahan yang akan Anda dapatkan terlihat seperti ini:
$ scarb cairo-run --available-gas=200000000
error: Missing token TerminalRParen.
--> src/lib.cairo:2:14
let x = (let y = 6);
^
error: Missing token TerminalSemicolon.
--> src/lib.cairo:2:14
let x = (let y = 6);
^
error: Missing token TerminalSemicolon.
--> src/lib.cairo:2:14
let x = (let y = 6);
^
error: Skipped tokens. Expected: statement.
--> src/lib.cairo:2:14
let x = (let y = 6);
Pernyataan let y = 6
tidak mengembalikan nilai, sehingga tidak ada yang dapat diikat oleh x
. Ini berbeda dengan apa yang terjadi dalam bahasa lain, seperti C dan Ruby, di mana penugasan mengembalikan nilai dari penugasan tersebut. Dalam bahasa-bahasa itu, Anda dapat menulis x = y = 6
dan memiliki kedua x
dan y
memiliki nilai 6
; itu tidak terjadi dalam Cairo.
Ekspresi mengevaluasi menjadi nilai dan membentuk sebagian besar kode yang akan Anda tulis di Cairo. Pertimbangkan operasi matematika, seperti 5 + 6
, yang merupakan ekspresi yang mengevaluasi menjadi nilai 11
. Ekspresi dapat menjadi bagian dari pernyataan: dalam Listing 2-1, 6
dalam pernyataan let y = 6;
adalah ekspresi yang mengevaluasi menjadi nilai 6
. Memanggil suatu fungsi adalah ekspresi. Blok cakupan baru yang dibuat dengan tanda kurung kurawal adalah ekspresi, misalnya:
{{#include ../listings/ch02-common-programming-concepts/no_listing_19_blocks_are_expressions/src/lib.cairo:all}}
Ekspresi ini:
{{#include ../listings/ch02-common-programming-concepts/no_listing_19_blocks_are_expressions/src/lib.cairo:block_expr}}
adalah blok yang, dalam hal ini, mengevaluasi menjadi 4
. Nilai tersebut diikatkan ke y
sebagai bagian dari pernyataan let
. Perhatikan bahwa baris x + 1
tidak memiliki titik koma di akhir, yang berbeda dari sebagian besar baris yang telah Anda lihat sejauh ini. Ekspresi tidak mencakup titik koma di akhir. Jika Anda menambahkan titik koma di akhir ekspresi, Anda mengubahnya menjadi pernyataan, dan itu tidak akan mengembalikan nilai. Ingatlah ini saat Anda menjelajahi nilai kembalian fungsi dan ekspresi selanjutnya.
Fungsi dengan Nilai Kembalian
Fungsi dapat mengembalikan nilai ke kode yang memanggilnya. Kita tidak memberi nama pada nilai kembalian, tetapi kita harus mendeklarasikan tipe mereka setelah tanda panah (->
). Di Cairo, nilai kembalian fungsi adalah sinonim dengan nilai dari ekspresi terakhir dalam blok tubuh fungsi. Anda dapat mengembalikan secara cepat dari fungsi dengan menggunakan kata kunci return
dan menentukan nilai, tetapi sebagian besar fungsi mengembalikan ekspresi terakhir secara implisit. Berikut adalah contoh fungsi yang mengembalikan nilai:
{{#include ../listings/ch02-common-programming-concepts/no_listing_20_function_return_values/src/lib.cairo}}
Tidak ada pemanggilan fungsi, atau bahkan pernyataan let
dalam fungsi five
—hanya angka 5
itu sendiri. Itu adalah fungsi yang sepenuhnya valid di Cairo. Perhatikan bahwa tipe pengembalian fungsi juga ditentukan, yaitu -> u32
. Coba jalankan kode ini; keluarannya seharusnya terlihat seperti ini:
$ scarb cairo-run --available-gas=200000000
x = 5
Run completed successfully, returning []
Remaining gas: 1999847760
Angka 5
dalam fungsi five
adalah nilai kembalian fungsi, itulah mengapa tipe pengembalian adalah u32
. Mari kita periksa ini lebih detail. Ada dua bagian penting: pertama, baris let x = five();
menunjukkan bahwa kita menggunakan nilai kembalian suatu fungsi untuk menginisialisasi sebuah variabel. Karena fungsi five
mengembalikan 5
, baris tersebut sama dengan yang berikut:
let x = 5;
Kedua, fungsi five
tidak memiliki parameter dan mendefinisikan tipe nilai kembali, tetapi tubuh fungsi tersebut adalah angka 5
yang sepi tanpa titik koma karena itu adalah ekspresi yang nilainya ingin kita kembalikan.
Mari kita lihat contoh lain:
{{#include ../listings/ch02-common-programming-concepts/no_listing_21_function_return_values_2/src/lib.cairo}}
Menjalankan kode ini akan mencetak x = 6
. Tetapi jika kita menempatkan titik koma di akhir baris yang berisi x + 1
, mengubahnya dari ekspresi menjadi pernyataan, kita akan mendapatkan kesalahan:
{{#include ../listings/ch02-common-programming-concepts/no_listing_22_function_return_invalid/src/lib.cairo}}
Mengompilasi kode ini menghasilkan kesalahan, seperti berikut:
error: Unexpected return type. Expected: "core::integer::u32", found: "()".
Pesan kesalahan utama, Unexpected return type
, mengungkapkan isu inti dengan kode ini. Definisi fungsi plus_one
menyatakan bahwa itu akan mengembalikan u32
, tetapi pernyataan tidak mengevaluasi menjadi nilai, yang dinyatakan oleh ()
, tipe unit. Oleh karena itu, tidak ada yang dikembalikan, yang bertentangan dengan definisi fungsi dan menghasilkan kesalahan.
Comments
Dalam program Cairo, Anda dapat menyertakan teks penjelasan di dalam kode menggunakan komentar. Untuk membuat komentar, gunakan sintaks //, setelah itu teks apa pun pada baris yang sama akan diabaikan oleh kompiler.
{{#include ../listings/ch02-common-programming-concepts/no_listing_23_comments/src/lib.cairo}}
Alur Kontrol
Kemampuan untuk menjalankan beberapa kode tergantung pada apakah suatu kondisi benar dan untuk menjalankan beberapa kode secara berulang saat suatu kondisi benar adalah blok dasar dalam kebanyakan bahasa pemrograman. Konstruksi paling umum yang memungkinkan Anda mengontrol alur eksekusi kode Cairo adalah ekspresi if
dan perulangan.
Ekspresi if
Ekspresi if
memungkinkan Anda bercabang dalam kode Anda tergantung pada kondisi. Anda memberikan kondisi dan kemudian menyatakan, "Jika kondisi ini terpenuhi, jalankan blok kode ini. Jika kondisinya tidak terpenuhi, jangan jalankan blok kode ini."
Nama file: src/lib.cairo
{{#include ../listings/ch02-common-programming-concepts/no_listing_24_if/src/lib.cairo}}
Semua ekspresi if
dimulai dengan kata kunci if
, diikuti oleh suatu kondisi. Dalam kasus ini, kondisinya memeriksa apakah variabel number
memiliki nilai yang sama dengan 5. Kami menempatkan blok kode yang akan dieksekusi jika kondisinya true
langsung setelah kondisi di dalam kurung kurawal.
Opsional, kita juga dapat menyertakan ekspresi else
, yang kita pilih lakukan di sini, untuk memberikan program blok kode alternatif untuk dieksekusi jika kondisinya dinilai false
. Jika Anda tidak menyediakan ekspresi else
dan kondisinya false
, program hanya akan melewati blok if
dan melanjutkan ke bit kode berikutnya.
Coba jalankan kode ini; Anda seharusnya melihat output berikut:
$ cairo-run main.cairo
[DEBUG] kondisi salah
Mari coba ubah nilai number
menjadi nilai yang membuat kondisinya menjadi true
untuk melihat apa yang terjadi:
let number = 5;
$ cairo-run main.cairo
kondisi benar
Perlu dicatat bahwa kondisi dalam kode ini harus berupa tipe data bool. Jika kondisinya bukan bool, kita akan mendapatkan kesalahan.
$ cairo-run main.cairo
thread 'main' panicked at 'Failed to specialize: `enum_match<felt252>`. Error: Could not specialize libfunc `enum_match` with generic_args: [Type(ConcreteTypeId { id: 1, debug_name: None })]. Error: Provided generic argument is unsupported.', crates/cairo-lang-sierra-generator/src/utils.rs:256:9
Menangani Kondisi Berganda dengan else if
Anda dapat menggunakan beberapa kondisi dengan menggabungkan if
dan else
dalam ekspresi else if
. Contohnya:
Nama file: src/lib.cairo
{{#include ../listings/ch02-common-programming-concepts/no_listing_25_else_if/src/lib.cairo}}
Program ini memiliki empat jalur yang mungkin dapat diambil. Setelah dijalankan, Anda seharusnya melihat output berikut:
number is 3
Run completed successfully, returning []
Remaining gas: 1999937120
Ketika program ini dijalankan, itu memeriksa setiap ekspresi if
secara bergantian dan mengeksekusi blok pertama di mana kondisinya dinilai true
. Perlu diperhatikan bahwa meskipun number - 2 == 1
adalah true
, kita tidak melihat output number minus 2 is 1'
dan juga tidak melihat teks number not found
dari blok else
. Hal ini karena Cairo hanya mengeksekusi blok untuk kondisi pertama yang benar, dan begitu menemukannya, ia bahkan tidak memeriksa yang lain. Menggunakan terlalu banyak ekspresi else if
dapat membuat kode Anda berantakan, jadi jika Anda memiliki lebih dari satu, Anda mungkin ingin melakukan refaktor pada kode Anda. Bab 6 menjelaskan suatu konstruksi percabangan Cairo yang kuat yang disebut match
untuk kasus-kasus seperti ini.
Menggunakan if
dalam pernyataan let
Karena if
adalah suatu ekspresi, kita dapat menggunakannya di sisi kanan dari pernyataan let
untuk menetapkan hasilnya ke suatu variabel.
Nama file: src/lib.cairo
{{#include ../listings/ch02-common-programming-concepts/no_listing_26_if_let/src/lib.cairo}}
$ cairo-run main.cairo
kondisi benar dan number adalah 5
Run completed successfully, returning []
Remaining gas: 1999780390
Variabel number
akan diikat ke suatu nilai berdasarkan hasil ekspresi if
. Yang akan menjadi 5 dalam contoh ini.
Pengulangan dengan Perulangan
Seringkali berguna untuk menjalankan blok kode lebih dari sekali. Untuk tugas ini, Cairo menyediakan sintaks loop sederhana, yang akan menjalankan kode di dalam tubuh loop hingga selesai, lalu segera memulainya kembali dari awal. Untuk bereksperimen dengan pengulangan, mari buat proyek baru yang disebut loops.
Saat ini, Cairo hanya memiliki satu jenis loop: loop
.
Mengulang Kode dengan loop
Kata kunci loop
memberi tahu Cairo untuk menjalankan blok kode berulang kali
selamanya atau sampai Anda secara eksplisit memberi tahu untuk berhenti.
Sebagai contoh, ubah file src/lib.cairo di direktori loops Anda agar terlihat seperti ini:
Nama file: src/lib.cairo
{{#include ../listings/ch02-common-programming-concepts/no_listing_27_loop/src/lib.cairo}}
Ketika kita menjalankan program ini, kita akan melihat again!
dicetak terus-menerus
hingga kita menghentikan program secara manual, karena kondisi berhenti tidak pernah tercapai.
Meskipun kompiler mencegah kita menulis program tanpa kondisi berhenti (break
statement),
kondisi berhenti mungkin tidak pernah tercapai, menghasilkan pengulangan tak terbatas.
Sebagian besar terminal mendukung pintasan keyboard ctrl-c untuk menghentikan program yang terjebak dalam pengulangan terus-menerus. Cobalah:
$ scarb cairo-run --available-gas=20000000
[DEBUG] again (raw: 418346264942)
[DEBUG] again (raw: 418346264942)
[DEBUG] again (raw: 418346264942)
[DEBUG] again (raw: 418346264942)
Run panicked with err values: [375233589013918064796019]
Remaining gas: 1050
Catatan: Cairo mencegah kita menjalankan program dengan pengulangan tak terbatas dengan menyertakan meteran gas. Meteran gas adalah mekanisme yang membatasi jumlah komputasi yang dapat dilakukan dalam suatu program. Dengan mengatur nilai ke flag
--available-gas
, kita dapat menetapkan jumlah gas maksimum yang tersedia untuk program. Gas adalah satuan pengukuran yang mengekspresikan biaya komputasi dari suatu instruksi. Ketika meteran gas habis, program akan berhenti. Dalam kasus ini, program mengalami kegagalan karena kehabisan gas, karena kondisi berhenti tidak pernah tercapai. Hal ini terutama penting dalam konteks kontrak pintar yang diterapkan di Starknet, karena mencegah dari menjalankan pengulangan tak terbatas di jaringan. Jika Anda menulis program yang perlu menjalankan pengulangan, Anda perlu menjalankannya dengan flag--available-gas
diatur ke nilai yang cukup besar untuk menjalankan program.
Untuk keluar dari loop, Anda dapat menempatkan pernyataan break
dalam loop untuk memberi tahu program kapan harus berhenti
mengeksekusi loop. Mari perbaiki pengulangan tak terbatas dengan membuat kondisi berhenti i > 10
dapat dicapai.
{{#include ../listings/ch02-common-programming-concepts/no_listing_28_loop_break/src/lib.cairo}}
Kata kunci continue
memberi tahu program untuk melanjutkan ke iterasi berikutnya dari loop dan melewati sisa kode dalam iterasi ini. Mari tambahkan pernyataan continue
ke loop kami untuk melewati pernyataan print
ketika i
sama dengan 5
.
use core::debug::PrintTrait;
fn main() {
let mut i: usize = 0;
loop {
if i > 10 {
break;
}
if i == 5 {
i += 1;
continue;
}
i.print();
i += 1;
}
}
Menjalankan program ini tidak akan mencetak nilai i
ketika i
sama dengan 5
.
Mengembalikan Nilai dari Loop
Salah satu penggunaan dari loop
adalah untuk mencoba operasi yang Anda tahu mungkin gagal,
seperti memeriksa apakah operasi tersebut berhasil. Anda mungkin juga perlu meneruskan
hasil dari operasi tersebut keluar dari loop ke bagian lain dari kode Anda. Untuk melakukan
ini, Anda dapat menambahkan nilai yang ingin Anda kembalikan setelah ekspresi break
yang
Anda gunakan untuk menghentikan loop; nilai tersebut akan dikembalikan dari loop sehingga Anda bisa
menggunakannya, seperti yang ditunjukkan di sini:
{{#include ../listings/ch02-common-programming-concepts/no_listing_29_loop_return_values/src/lib.cairo}}
Sebelum loop, kita mendeklarasikan variabel bernama counter
dan menginisialisasinya dengan
0
. Kemudian kita mendeklarasikan variabel bernama result
untuk menyimpan nilai yang dikembalikan dari
loop. Pada setiap iterasi loop, kita memeriksa apakah counter
sama dengan 10
, dan kemudian menambahkan 1
ke variabel counter
.
Ketika kondisi terpenuhi, kita menggunakan kata kunci break
dengan nilai counter * 2
. Setelah loop, kita menggunakan titik koma untuk mengakhiri pernyataan yang menetapkan nilai ke result
. Akhirnya, kita
mencetak nilai di result
, yang dalam hal ini adalah 20
.
Ringkasan
Anda berhasil! Ini adalah bab yang cukup besar: Anda belajar tentang variabel, tipe data, fungsi, komentar,
ekspresi if
, dan loop! Untuk berlatih dengan konsep-konsep yang dibahas dalam bab ini,
cobalah membangun program untuk melakukan hal-hal berikut:
- Menghasilkan bilangan Fibonacci ke-n-th.
- Menghitung faktorial dari suatu bilangan n.
Selanjutnya, kita akan meninjau jenis koleksi umum di Cairo pada bab berikutnya.
Koleksi Umum
Cairo menyediakan serangkaian jenis koleksi umum yang dapat digunakan untuk menyimpan dan memanipulasi data. Koleksi-koleksi ini dirancang agar efisien, fleksibel, dan mudah digunakan. Bagian ini memperkenalkan jenis koleksi utama yang tersedia di Cairo: Array dan Dictionary.
Array
Sebuah array adalah kumpulan elemen yang memiliki tipe yang sama. Anda dapat membuat dan menggunakan metode array dengan menggunakan ArrayTrait
trait dari pustaka inti.
Hal penting yang perlu dicatat adalah bahwa array memiliki opsi modifikasi yang terbatas. Array sebenarnya adalah antrian nilai yang tidak dapat diubah.
Ini terkait dengan fakta bahwa setelah suatu slot memori ditulis, itu tidak dapat ditimpa, tetapi hanya dapat dibaca. Anda hanya dapat menambahkan item ke ujung array dan menghapus item dari depan menggunakan pop_front
.
Membuat Array
Membuat array dilakukan dengan panggilan ArrayTrait::new()
. Berikut adalah contoh pembuatan array yang kami tambahkan 3 elemen ke dalamnya:
{{#include ../listings/ch03-common-collections/no_listing_00_array_new_append/src/lib.cairo}}
Jika diperlukan, Anda dapat menyertakan tipe yang diharapkan dari item di dalam array saat menginstansiasi array seperti ini, atau secara eksplisit menentukan tipe variabelnya.
let mut arr = ArrayTrait::<u128>::new();
let mut arr:Array<u128> = ArrayTrait::new();
Memperbarui Array
Menambahkan Elemen
Untuk menambahkan elemen ke ujung array, Anda dapat menggunakan metode append()
:
{{#rustdoc_include ../listings/ch03-common-collections/no_listing_00_array_new_append/src/lib.cairo:5}}
Menghapus Elemen
Anda hanya dapat menghapus elemen dari depan array dengan menggunakan metode pop_front()
. Metode ini mengembalikan Option
yang berisi elemen yang dihapus, atau Option::None
jika array kosong.
{{#include ../listings/ch03-common-collections/no_listing_01_array_pop_front/src/lib.cairo}}
Kode di atas akan mencetak 10
karena kita menghapus elemen pertama yang ditambahkan.
Di Cairo, memori bersifat tidak dapat diubah, yang berarti tidak mungkin mengubah elemen-elemen array setelah ditambahkan. Anda hanya dapat menambahkan elemen ke ujung array dan menghapus elemen dari depan array. Operasi-operasi ini tidak memerlukan mutasi memori, karena melibatkan pembaruan pointer daripada langsung mengubah sel-sel memori.
Membaca Elemen dari Array
Untuk mengakses elemen-elemen array, Anda dapat menggunakan metode array get()
atau at()
yang mengembalikan tipe yang berbeda. Menggunakan arr.at(index)
setara dengan menggunakan operator subscripting arr[index]
.
Fungsi get
mengembalikan Option<Box<@T>>
, yang berarti mengembalikan opsi untuk tipe Box (tipe smart-pointer Cairo) yang berisi snapshot elemen pada indeks yang ditentukan jika elemen tersebut ada dalam array. Jika elemen tidak ada, get
mengembalikan None
. Metode ini berguna ketika Anda berharap mengakses indeks yang mungkin tidak berada dalam batas array dan ingin menangani kasus tersebut dengan lembut tanpa panic. Snapshot akan dijelaskan lebih detail dalam bab Referensi dan Snapshot.
Fungsi at
, di sisi lain, langsung mengembalikan snapshot elemen pada indeks yang ditentukan menggunakan operator unbox()
untuk mengekstrak nilai yang disimpan dalam kotak. Jika indeks di luar batas, terjadi kesalahan panic. Anda sebaiknya hanya menggunakan at
ketika Anda ingin program panic jika indeks yang diberikan berada di luar batas array, yang dapat mencegah perilaku yang tidak terduga.
Secara ringkas, gunakan at
ketika Anda ingin panic pada percobaan akses di luar batas, dan gunakan get
ketika Anda lebih suka menangani kasus tersebut dengan lembut tanpa panic.
{{#include ../listings/ch03-common-collections/no_listing_02_array_at/src/lib.cairo}}
Dalam contoh ini, variabel yang dinamai first
akan mendapatkan nilai 0
karena itu adalah nilai pada indeks 0
dalam array. Variabel yang dinamai second
akan mendapatkan nilai 1
dari indeks 1
dalam array.
Berikut adalah contoh dengan metode get()
:
{{#include ../listings/ch03-common-collections/no_listing_03_array_get/src/lib.cairo}}
Metode Terkait Ukuran
Untuk menentukan jumlah elemen dalam sebuah array, gunakan metode len()
. Hasilnya adalah tipe usize
.
Jika Anda ingin memeriksa apakah sebuah array kosong atau tidak, Anda dapat menggunakan metode is_empty()
, yang mengembalikan true
jika array kosong dan false
sebaliknya.
Menyimpan Tipe-Tipe Berbeda dengan Enums
Jika Anda ingin menyimpan elemen-elemen dengan tipe yang berbeda dalam sebuah array, Anda dapat menggunakan Enum
untuk mendefinisikan tipe data kustom yang dapat menyimpan tipe-tipe berbeda. Enums akan dijelaskan lebih detail dalam bab Enums and Pattern Matching.
{{#include ../listings/ch03-common-collections/no_listing_04_array_with_enums/src/lib.cairo}}
Span
Span
adalah struktur data yang mewakili snapshot dari sebuah Array
. Dirancang untuk memberikan akses aman dan terkontrol ke elemen-elemen array tanpa memodifikasi array asli. Span sangat berguna untuk memastikan integritas data dan menghindari masalah peminjaman saat melewatkan array antar fungsi atau saat melakukan operasi hanya baca (lihat References and Snapshots).
Semua metode yang disediakan oleh Array
juga dapat digunakan dengan Span
, kecuali metode append()
.
Mengubah Array menjadi Span
Untuk membuat Span
dari sebuah Array
, panggil metode span()
:
{{#rustdoc_include ../listings/ch03-common-collections/no_listing_05_array_span/src/lib.cairo:3}}
Kamus
Cairo menyediakan dalam perpustakaan intinya tipe data mirip kamus. Tipe data Felt252Dict<T>
mewakili kumpulan pasangan kunci-nilai di mana setiap kunci bersifat unik dan terkait dengan nilai yang sesuai. Tipe struktur data ini dikenal dengan nama berbeda dalam berbagai bahasa pemrograman seperti peta, tabel hash, larik asosiatif, dan banyak lainnya.
Tipe Felt252Dict<T>
berguna ketika Anda ingin mengorganisir data Anda dengan cara tertentu di mana menggunakan Array<T>
dan pengindeksan tidak cukup. Kamus Cairo juga memungkinkan pemrogram untuk dengan mudah mensimulasikan keberadaan memori yang dapat diubah ketika tidak ada.
Penggunaan Dasar Kamus
Biasanya dalam bahasa lain ketika membuat kamus baru, kita harus mendefinisikan tipe data baik untuk kunci maupun nilai. Di Cairo, tipe kunci dibatasi menjadi felt252
, meninggalkan hanya kemungkinan untuk menentukan tipe data nilai, yang direpresentasikan oleh T
dalam Felt252Dict<T>
.
Fungsionalitas inti dari Felt252Dict<T>
diimplementasikan dalam trait Felt252DictTrait
yang mencakup semua operasi dasar. Di antaranya, kita dapat menemukan:
insert(felt252, T) -> ()
untuk menulis nilai ke instance kamus danget(felt252) -> T
untuk membaca nilai darinya.
Fungsi-fungsi ini memungkinkan kita untuk memanipulasi kamus seperti dalam bahasa pemrograman lain. Pada contoh berikut, kita membuat kamus untuk merepresentasikan pemetaan antara individu dan saldo mereka:
{{#include ../listings/ch03-common-collections/no_listing_07_intro/src/lib.cairo}}
Kita dapat membuat instance baru dari Felt252Dict<u64>
dengan menggunakan metode default
dari trait Default
dan menambahkan dua individu, masing-masing dengan saldo mereka sendiri, menggunakan metode insert
. Akhirnya, kita memeriksa saldo pengguna dengan metode get
. Metode-metode ini didefinisikan dalam trait Felt252DictTrait
dalam perpustakaan inti.
Sepanjang buku ini, kita telah membahas bagaimana memori Cairo bersifat tidak dapat diubah, yang berarti Anda hanya dapat menulis ke sel memori sekali, tetapi tipe Felt252Dict<T>
mewakili cara untuk mengatasi hambatan ini. kita akan menjelaskan bagaimana hal ini diimplementasikan nanti pada bagian Dictionaries Underneath.
Berlanjut dari contoh sebelumnya, mari tunjukkan contoh kode di mana saldo pengguna yang sama berubah:
{{#include ../listings/ch03-common-collections/no_listing_08_intro_rewrite/src/lib.cairo}}
Perhatikan bagaimana dalam contoh ini kita menambahkan individu Alex dua kali, setiap kali menggunakan saldo yang berbeda dan setiap kali kita memeriksa saldo, itu memiliki nilai terakhir yang dimasukkan! Felt252Dict<T>
efektif memungkinkan kita "menulis ulang" nilai yang disimpan untuk kunci apa pun.
Sebelum melanjutkan dan menjelaskan bagaimana kamus diimplementasikan, penting untuk dicatat bahwa begitu Anda menginisiasi Felt252Dict<T>
, di belakang layar semua kunci memiliki nilai yang terkait diinisialisasi sebagai nol. Ini berarti bahwa jika misalnya Anda mencoba mendapatkan saldo pengguna yang tidak ada, Anda akan mendapatkan 0 daripada kesalahan atau nilai yang tidak terdefinisi. Ini juga berarti tidak ada cara untuk menghapus data dari kamus. Sesuatu yang perlu diperhatikan ketika menggabungkan struktur ini ke dalam kode Anda.
Hingga saat ini, kita telah melihat semua fitur dasar dari Felt252Dict<T>
dan bagaimana ia meniru perilaku yang sama seperti struktur data yang sesuai dalam bahasa pemrograman lainnya, yaitu, secara eksternal tentu saja. Cairo pada intinya adalah bahasa pemrograman yang non-deterministik dan dapat diselesaikan oleh Turing, sangat berbeda dari bahasa populer lainnya yang ada, yang sebagai konsekuensinya berarti bahwa kamus diimplementasikan dengan cara yang sangat berbeda juga!
Pada bagian-bagian berikutnya, kita akan memberikan beberapa wawasan tentang mekanisme internal Felt252Dict<T>
dan kompromi yang diambil untuk membuatnya berfungsi. Setelah itu, kita akan melihat bagaimana menggunakan kamus dengan struktur data lain serta menggunakan metode entry
sebagai cara lain untuk berinteraksi dengan mereka.
Kamus di Dalamnya
Salah satu kendala dari desain non-deterministik Cairo adalah bahwa sistem memorinya bersifat tidak dapat diubah, sehingga untuk mensimulasikan mutabilitas, bahasa ini mengimplementasikan Felt252Dict<T>
sebagai daftar entri. Setiap entri mewakili waktu ketika kamus diakses untuk tujuan membaca/memperbarui/menulis. Sebuah entri memiliki tiga bidang:
- Bidang
key
yang mengidentifikasi nilai untuk pasangan kunci-nilai kamus ini. - Bidang
previous_value
yang menunjukkan nilai sebelumnya yang dipegang dikey
. - Bidang
new_value
yang menunjukkan nilai baru yang dipegang dikey
.
Jika kita mencoba mengimplementasikan Felt252Dict<T>
menggunakan struktur tingkat tinggi, kita akan mendefinisikannya secara internal sebagai Array<Entry<T>>
di mana setiap Entry<T>
memiliki informasi tentang pasangan kunci-nilai yang direpresentasikannya dan nilai-nilai sebelumnya dan baru yang dipegangnya. Definisi dari Entry<T>
akan menjadi sebagai berikut:
{{#include ../listings/ch03-common-collections/no_listing_09_entries/src/lib.cairo:struct}}
Setiap kali kita berinteraksi dengan Felt252Dict<T>
, entri baru Entry<T>
akan terdaftar:
- Sebuah
get
akan mendaftarkan entri di mana tidak ada perubahan dalam status, dan nilai-nilai sebelumnya dan baru disimpan dengan nilai yang sama. - Sebuah
insert
akan mendaftarkanEntry<T>
baru di mananew_value
akan menjadi elemen yang dimasukkan, danprevious_value
adalah elemen terakhir yang dimasukkan sebelumnya. Jika ini adalah entri pertama untuk kunci tertentu, maka nilai sebelumnya akan menjadi nol.
Penggunaan daftar entri ini menunjukkan bagaimana tidak ada penulisan ulang, hanya pembuatan sel memori baru per interaksi Felt252Dict<T>
. Mari tunjukkan contoh penggunaan kamus balances
dari bagian sebelumnya dan menyisipkan pengguna 'Alex' dan 'Maria':
{{#rustdoc_include ../listings/ch03-common-collections/no_listing_09_entries/src/lib.cairo:inserts}}
Instruksi-instruksi ini kemudian akan menghasilkan daftar entri berikut:
kunci | sebelumnya | baru |
---|---|---|
Alex | 0 | 100 |
Maria | 0 | 50 |
Alex | 100 | 200 |
Maria | 50 | 50 |
Perhatikan bahwa karena 'Alex' dimasukkan dua kali, itu muncul dua kali dan nilai sebelumnya
dan saat ini
diatur dengan benar. Juga membaca dari 'Maria' mendaftarkan entri tanpa perubahan dari nilai sebelumnya ke nilai saat ini.
Pendekatan ini untuk mengimplementasikan Felt252Dict<T>
berarti bahwa untuk setiap operasi baca/tulis, ada pemindaian seluruh daftar entri dalam pencarian entri terakhir dengan kunci yang sama. Begitu entri telah ditemukan, new_value
-nya diekstrak dan digunakan pada entri baru yang akan ditambahkan sebagai previous_value
. Ini berarti berinteraksi dengan Felt252Dict<T>
memiliki kompleksitas waktu terburuk O(n)
di mana n
adalah jumlah entri dalam daftar.
Jika Anda memikirkan cara alternatif untuk mengimplementasikan Felt252Dict<T>
, Anda pasti akan menemukannya, mungkin bahkan meninggalkan sepenuhnya kebutuhan untuk bidang previous_value
, meskipun, karena Cairo bukanlah bahasa normal Anda, hal ini tidak akan berfungsi. Salah satu tujuan Cairo adalah, dengan sistem bukti STARK, menghasilkan bukti integritas komputasional. Ini berarti Anda perlu memverifikasi bahwa eksekusi program benar dan berada dalam batas pembatasan Cairo. Salah satu pemeriksaan batas tersebut terdiri dari "penghancuran kamus" dan itu memerlukan informasi tentang nilai-nilai sebelumnya dan baru untuk setiap entri.
Penghancuran Kamus
Untuk memverifikasi bahwa bukti yang dihasilkan oleh eksekusi program Cairo yang menggunakan Felt252Dict<T>
benar, kita perlu memeriksa bahwa tidak ada manipulasi ilegal dengan kamus. Ini dilakukan melalui metode yang disebut squash_dict
yang meninjau setiap entri dari daftar entri dan memeriksa bahwa akses ke kamus tetap konsisten sepanjang eksekusi.
Proses penghancuran dilakukan sebagai berikut: diberikan semua entri dengan kunci tertentu k
, diambil dalam urutan yang sama dengan saat dimasukkan, verifikasi bahwa nilai new_value
pada entri ke-i sama dengan nilai previous_value
pada entri ke-i + 1.
Sebagai contoh, diberikan daftar entri berikut:
kunci | sebelumnya | baru |
---|---|---|
Alex | 0 | 150 |
Maria | 0 | 100 |
Charles | 0 | 70 |
Maria | 100 | 250 |
Alex | 150 | 40 |
Alex | 40 | 300 |
Maria | 250 | 190 |
Alex | 300 | 90 |
Setelah penghancuran, daftar entri akan dikurangi menjadi:
kunci | sebelumnya | baru |
---|---|---|
Alex | 0 | 90 |
Maria | 0 | 190 |
Charles | 0 | 70 |
Jika terjadi perubahan pada salah satu nilai tabel pertama, penghancuran akan gagal selama runtime.
Penghancuran Kamus
Jika Anda menjalankan contoh dari Penggunaan Dasar Kamus, Anda akan melihat bahwa tidak pernah ada panggilan untuk menyusun kamus, namun program berhasil dikompilasi. Apa yang terjadi di belakang layar adalah bahwa penyusunan secara otomatis dipanggil melalui implementasi Felt252Dict<T>
dari trait Destruct<T>
. Panggilan ini terjadi tepat sebelum kamus balance
keluar dari cakupan.
Trait Destruct<T>
mewakili cara lain untuk mengeluarkan instansi dari cakupan selain Drop<T>
. Perbedaan utama antara keduanya adalah bahwa Drop<T>
dianggap sebagai operasi no-op, yang berarti tidak menghasilkan CASM baru sementara Destruct<T>
tidak memiliki batasan ini. Satu-satunya tipe yang secara aktif menggunakan trait Destruct<T>
adalah Felt252Dict<T>
, untuk setiap tipe lainnya, Destruct<T>
dan Drop<T>
adalah sinonim. Anda dapat membaca lebih lanjut tentang trait ini di Drop and Destruct.
Nanti, pada bagian Kamus sebagai Anggota Struktur, kita akan memiliki contoh praktis di mana kita mengimplementasikan trait Destruct<T>
untuk tipe kustom.
Lebih Banyak Kamus
Hingga saat ini, kita telah memberikan gambaran menyeluruh tentang fungsionalitas Felt252Dict<T>
serta bagaimana dan mengapa itu diimplementasikan dengan cara tertentu. Jika Anda belum memahami semuanya, jangan khawatir karena dalam bagian ini kita akan memberikan beberapa contoh lebih lanjut menggunakan kamus.
kita akan memulai dengan menjelaskan metode entry
yang merupakan bagian dari fungsionalitas dasar kamus yang disertakan dalam Felt252DictTrait<T>
yang tidak kita sebutkan di awal. Segera setelah itu, kita akan melihat contoh bagaimana Felt252Dict<T>
berinteraksi dengan jenis kompleks lain seperti Array<T>
dan bagaimana mengimplementasikan sebuah struktur dengan kamus sebagai anggota.
Entry dan Finalisasi
Pada bagian Dictionaries Underneath, kita menjelaskan bagaimana Felt252Dict<T>
bekerja secara internal. Ini adalah daftar entri untuk setiap kali kamus diakses dengan cara apa pun. Pertama, ia akan menemukan entri terakhir dengan memberikan key
tertentu, dan kemudian memperbarui sesuai dengan operasi apa pun yang sedang dijalankan. Bahasa Cairo memberi kita alat untuk mereplikasikan ini sendiri melalui metode entry
dan finalize
.
Metode entry
datang sebagai bagian dari Felt252DictTrait<T>
dengan tujuan membuat entri baru dengan memberikan kunci tertentu. Begitu dipanggil, metode ini mengambil kepemilikan kamus dan mengembalikan entri untuk diperbarui. Tanda tangan metodenya adalah sebagai berikut:
fn entry(self: Felt252Dict<T>, key: felt252) -> (Felt252DictEntry<T>, T) nopanic
Parameter masukan pertama mengambil kepemilikan kamus sementara yang kedua digunakan untuk membuat entri yang sesuai. Ini mengembalikan tuple yang berisi Felt252DictEntry<T>
, yang merupakan tipe yang digunakan oleh Cairo untuk mewakili entri kamus, dan T
yang mewakili nilai yang dipegang sebelumnya.
Langkah berikutnya adalah memperbarui entri dengan nilai baru. Untuk ini, kita menggunakan metode finalize
yang menyisipkan entri dan mengembalikan kepemilikan kamus:
fn finalize(self: Felt252DictEntry<T>, new_value: T) -> Felt252Dict<T> {
Metode ini menerima entri dan nilai baru sebagai parameter dan mengembalikan kamus yang diperbarui.
Mari kita lihat contoh penggunaan entry
dan finalize
. Bayangkan kita ingin mengimplementasikan versi kustom dari metode get
dari kamus. Maka kita harus melakukan hal berikut:
- Buat entri baru untuk ditambahkan menggunakan metode
entry
. - Sisipkan kembali entri di mana
new_value
sama denganprevious_value
. - Kembalikan nilai.
Implementasi get
kustom kita akan terlihat seperti ini:
{{#include ../listings/ch03-common-collections/no_listing_10_custom_methods/src/lib.cairo:imports}}
{{#include ../listings/ch03-common-collections/no_listing_10_custom_methods/src/lib.cairo:custom_get}}
Implementasi metode insert
akan mengikuti alur kerja yang serupa, kecuali untuk menyisipkan nilai baru saat finalisasi. Jika kita hendak mengimplementasikannya, itu akan terlihat seperti berikut:
{{#include ../listings/ch03-common-collections/no_listing_10_custom_methods/src/lib.cairo:imports}}
{{#include ../listings/ch03-common-collections/no_listing_10_custom_methods/src/lib.cairo:custom_insert}}
Sebagai catatan penutup, kedua metode ini diimplementasikan dengan cara yang mirip dengan bagaimana insert
dan get
diimplementasikan untuk Felt252Dict<T>
. Kode ini menunjukkan beberapa penggunaan contoh:
{{#rustdoc_include ../listings/ch03-common-collections/no_listing_10_custom_methods/src/lib.cairo:main}}
Kamus dari Jenis yang Tidak Didukung Secara Asli
Salah satu pembatasan dari Felt252Dict<T>
yang belum kita bahas adalah trait Felt252DictValue<T>
.
Trait ini mendefinisikan metode zero_default
yang dipanggil ketika nilai tidak ada dalam kamus.
Ini diimplementasikan oleh beberapa tipe data umum, seperti sebagian besar bilangan bulat tak bertanda, bool
, dan felt252
, tetapi tidak diimplementasikan untuk tipe yang lebih kompleks seperti larik, struktur (termasuk u256
), dan tipe lain dari perpustakaan inti.
Ini berarti membuat kamus dari jenis yang tidak didukung secara alami tidaklah tugas yang mudah, karena Anda perlu menulis beberapa implementasi trait untuk membuat tipe data menjadi tipe nilai kamus yang valid.
Sebagai gantinya, Anda dapat melingkupi tipe Anda dalam Nullable<T>
.
Nullable<T>
adalah tipe penunjuk pintar yang dapat menunjuk pada nilai atau menjadi null
dalam ketiadaan nilai. Biasanya digunakan dalam Bahasa Pemrograman Berorientasi Objek ketika referensi tidak menunjuk ke mana-mana. Perbedaannya dengan Option
adalah bahwa nilai yang dilingkupi disimpan di dalam tipe data Box<T>
. Tipe Box<T>
, terinspirasi oleh Rust, memungkinkan kita mengalokasikan segmen memori baru untuk tipe kita, dan mengakses segmen ini menggunakan penunjuk yang hanya dapat dimanipulasi di satu tempat pada satu waktu.
Mari tunjukkan dengan contoh. kita akan mencoba menyimpan Span<felt252>
di dalam kamus. Untuk itu, kita akan menggunakan Nullable<T>
dan Box<T>
. Juga, kita menyimpan Span<T>
dan bukan Array<T>
karena yang terakhir tidak mengimplementasikan trait Copy<T>
yang diperlukan untuk membaca dari kamus.
{{#include ../listings/ch03-common-collections/no_listing_11_dict_of_complex/src/lib.cairo:imports}}
{{#include ../listings/ch03-common-collections/no_listing_11_dict_of_complex/src/lib.cairo:header}}
//...
Dalam potongan kode ini, hal pertama yang kita lakukan adalah membuat kamus baru d
. kita ingin kamus ini menyimpan Nullable<Span>
. Setelah itu, kita membuat larik dan mengisinya dengan nilai.
Langkah terakhir adalah menyisipkan larik sebagai span ke dalam kamus. Perhatikan bahwa kita tidak melakukan itu secara langsung, tetapi sebaliknya, kita mengambil beberapa langkah di antaranya:
- kita melingkupi larik dalam
Box
menggunakan metodenew
dariBoxTrait
. - kita melingkupi
Box
dalam bentuk yang dapat dinyatakan nol menggunakan fungsinullable_from_box
. - Akhirnya, kita menyisipkan hasilnya.
Setelah elemen berada di dalam kamus, dan kita ingin mendapatkannya, kita ikuti langkah yang sama tetapi dalam urutan terbalik. Kode berikut menunjukkan bagaimana melakukannya:
//...
{{#include ../listings/ch03-common-collections/no_listing_11_dict_of_complex/src/lib.cairo:footer}}
Di sini kita:
- Membaca nilai menggunakan
get
. - Memverifikasi bahwa itu tidak null menggunakan fungsi
match_nullable
. - Membuka nilai di dalam kotak dan memastikan bahwa itu benar.
Skrip lengkap akan terlihat seperti ini:
{{#include ../listings/ch03-common-collections/no_listing_11_dict_of_complex/src/lib.cairo:all}}
Kamus sebagai Anggota Struct
Menentukan kamus sebagai anggota struct memungkinkan di Cairo, tetapi berinteraksi dengan mereka mungkin tidak sepenuhnya lancar. Mari mencoba mengimplementasikan database pengguna kustom yang akan memungkinkan kita menambahkan pengguna dan mengajukannya. Kita perlu menentukan struct untuk mewakili tipe baru dan trait untuk menentukan fungsinya:
{{#include ../listings/ch03-common-collections/no_listing_12_dict_struct_member/src/lib.cairo:struct}}
{{#include ../listings/ch03-common-collections/no_listing_12_dict_struct_member/src/lib.cairo:trait}}
Tipe baru kita UserDatabase<T>
mewakili database pengguna. Ini generic terhadap saldo pengguna, memberikan fleksibilitas utama kepada siapa pun yang menggunakan tipe data kita. Dua anggotanya adalah:
users_amount
, jumlah pengguna yang saat ini dimasukkan, danbalances
, pemetaan setiap pengguna ke saldo mereka.
Fungsionalitas inti database didefinisikan oleh UserDatabaseTrait
. Metode-metode berikut didefinisikan:
new
untuk membuat tipeUserDatabase
baru dengan mudah.add_user
untuk menyisipkan pengguna ke dalam database.get_balance
untuk menemukan saldo pengguna di dalam database.
Langkah terakhir yang tersisa adalah mengimplementasikan masing-masing metode di UserDatabaseTrait
, tetapi karena kita bekerja dengan jenis generik, kita juga perlu menetapkan persyaratan T
dengan benar sehingga itu dapat menjadi nilai tipe Felt252Dict<T>
yang valid:
T
harus mengimplementasikanCopy<T>
karena diperlukan untuk mendapatkan nilai dariFelt252Dict<T>
.- Semua jenis nilai dari kamus mengimplementasikan
Felt252DictValue<T>
, tipe generic kita juga harus melakukannya. - Untuk menyisipkan nilai,
Felt252DictTrait<T>
mengharuskan semua jenis nilai untuk dapat dihancurkan.
Implementasinya, dengan semua pembatasan di tempat, akan sebagai berikut:
{{#include ../listings/ch03-common-collections/no_listing_12_dict_struct_member/src/lib.cairo:imports}}
{{#include ../listings/ch03-common-collections/no_listing_12_dict_struct_member/src/lib.cairo:impl}}
Implementasi database kita hampir selesai, kecuali satu hal: kompiler tidak tahu bagaimana membuat UserDatabase<T>
keluar dari cakupan, karena tidak mengimplementasikan trait Drop<T>
atau trait Destruct<T>
. Karena memiliki Felt252Dict<T>
sebagai anggota, itu tidak dapat dihapus, jadi kita terpaksa mengimplementasikan trait Destruct<T>
secara manual (lihat bab Milik untuk informasi lebih lanjut). Menggunakan #[derive(Destruct)]
di atas definisi UserDatabase<T>
tidak akan berhasil karena penggunaan generik dalam definisi struct. Kita perlu menulis implementasi trait Destruct<T>
sendiri:
{{#include ../listings/ch03-common-collections/no_listing_12_dict_struct_member/src/lib.cairo:destruct}}
Mengimplementasikan Destruct<T>
untuk UserDatabase
adalah langkah terakhir kita untuk mendapatkan database yang sepenuhnya fungsional. Sekarang kita bisa mencobanya:
{{#rustdoc_include ../listings/ch03-common-collections/no_listing_12_dict_struct_member/src/lib.cairo:main}}
Ringkasan
Selamat! Anda telah menyelesaikan bab ini tentang array dan kamus di Cairo. Struktur data ini mungkin sedikit sulit dipahami, tetapi mereka sangat berguna.
Ketika Anda siap untuk melanjutkan, kita akan membahas konsep yang Cairo bagikan dengan Rust dan tidak biasa ada di bahasa pemrograman lain: kepemilikan.
Struktur Data Kustom
Ketika Anda pertama kali mulai memprogram di Cairo, kemungkinan besar Anda ingin menggunakan array (Array<T>
) untuk menyimpan kumpulan data. Namun, Anda akan segera menyadari bahwa array memiliki satu keterbatasan besar - data yang disimpan di dalamnya bersifat tidak dapat diubah. Begitu Anda menambahkan nilai ke dalam array, Anda tidak dapat memodifikasinya.
Hal ini dapat menjadi frustrasi ketika Anda ingin menggunakan struktur data yang dapat diubah. Misalnya, katakanlah Anda membuat sebuah game di mana para pemain memiliki level, dan mereka dapat naik level. Anda mungkin mencoba menyimpan level pemain dalam sebuah array:
let mut level_pemain = Array::new();
level_pemain.append(5);
level_pemain.append(1);
level_pemain.append(10);
Namun, kemudian Anda menyadari bahwa Anda tidak dapat meningkatkan level di indeks tertentu setelah diatur. Jika seorang pemain mati, Anda tidak dapat menghapusnya dari array kecuali dia kebetulan berada di posisi pertama.
Untungnya, Cairo menyediakan tipe kamus bawaan yang praktis disebut Felt252Dict<T>
yang memungkinkan kita mensimulasikan perilaku struktur data yang dapat diubah. Mari pertama-tama jelajahi cara menggunakannya untuk membuat implementasi array dinamis.
Catatan: Beberapa konsep yang digunakan dalam bab ini disajikan di bagian-bagian berikutnya dari buku ini. Kami menyarankan Anda untuk membaca bab berikut terlebih dahulu: Strukt, Metode, Tipe Generik, Traits
Mensimulasikan array dinamis dengan kamus
Pertama, mari pikirkan bagaimana kita ingin array dinamis kita bersikap. Operasi apa yang harus didukung?
Array dinamis kita harus:
- Memungkinkan kita untuk menambahkan item di akhir
- Memungkinkan kita mengakses item apa pun berdasarkan indeks
- Memungkinkan menetapkan nilai item di indeks tertentu
- Mengembalikan panjang saat ini
Kita dapat mendefinisikan antarmuka ini di Cairo seperti:
{{#include ../listings/ch03-common-collections/no_listing_13_cust_struct_vect/src/lib.cairo:trait}}
Ini memberikan blueprint untuk implementasi array dinamis kita. Kami menamainya Vec karena mirip dengan struktur data Vec<T>
di Rust.
Mengimplementasikan array dinamis di Cairo
Untuk menyimpan data kita, kita akan menggunakan Felt252Dict<T>
yang memetakan nomor indeks (felts) ke nilai. Kita juga akan menyimpan bidang len
terpisah untuk melacak panjang.
Inilah tampilan struct kita. Kami membungkus tipe T
di dalam pointer Nullable
untuk memungkinkan penggunaan setiap tipe T
dalam struktur data kita, sebagaimana dijelaskan dalam bagian Kamus:
{{#include ../listings/ch03-common-collections/no_listing_13_cust_struct_vect/src/lib.cairo:struct}}
Hal utama yang membuat vektor ini dapat diubah adalah kita dapat menyisipkan nilai ke dalam kamus untuk menetapkan atau memperbarui nilai dalam struktur data kita. Misalnya, untuk memperbarui nilai di indeks tertentu, kita melakukan:
{{#include ../listings/ch03-common-collections/no_listing_13_cust_struct_vect/src/lib.cairo:set}}
Ini mengganti nilai yang sudah ada sebelumnya di indeks tersebut dalam kamus.
Sementara array bersifat tidak dapat diubah, kamus memberikan fleksibilitas yang kita butuhkan untuk struktur data yang dapat dimodifikasi seperti vektor.
Implementasi sisa antarmuka ini mudah dimengerti. Implementasi semua metode yang didefinisikan dalam antarmuka kami dapat dilakukan sebagai berikut:
{{#include ../listings/ch03-common-collections/no_listing_13_cust_struct_vect/src/lib.cairo:implem}}
Implementasi lengkap struktur Vec
dapat ditemukan dalam perpustakaan yang dijaga oleh komunitas Alexandria.
Mensimulasikan Stack dengan kamus
Sekarang kita akan melihat contoh kedua dan detail implementasinya: sebuah Stack.
Stack adalah koleksi LIFO (Last-In, First-Out). Penyisipan elemen baru dan penghapusan elemen yang ada terjadi di ujung yang sama, yang diwakili sebagai puncak tumpukan.
Mari kita tentukan operasi apa yang kita perlukan untuk membuat stack:
- Dorong item ke bagian atas tumpukan
- Pop item dari bagian atas tumpukan
- Periksa apakah masih ada elemen di tumpukan.
Dari spesifikasi ini, kita dapat menentukan antarmuka berikut:
{{#include ../listings/ch03-common-collections/no_listing_14_cust_struct_stack/src/lib.cairo:trait}}
Mengimplementasikan Stack yang Dapat Diubah di Cairo
Untuk membuat struktur data tumpukan di Cairo, kita bisa lagi menggunakan Felt252Dict<T>
untuk menyimpan nilai-nilai tumpukan bersama dengan bidang usize
untuk melacak panjang tumpukan agar bisa diiterasi.
Struktur Stack didefinisikan sebagai berikut:
{{#include ../listings/ch03-common-collections/no_listing_14_cust_struct_stack/src/lib.cairo:struct}}
Selanjutnya, mari lihat bagaimana fungsi utama kami push
dan pop
diimplementasikan.
{{#include ../listings/ch03-common-collections/no_listing_14_cust_struct_stack/src/lib.cairo:implem}}
Kode menggunakan metode insert
dan get
untuk mengakses nilai-nilai dalam Felt252Dict<T>
. Untuk mendorong elemen di bagian atas tumpukan, fungsi push
menyisipkan elemen ke dalam kamus di indeks len
- dan meningkatkan bidang len
dari tumpukan untuk melacak posisi puncak tumpukan. Untuk menghapus sebuah nilai, fungsi pop
mengambil nilai terakhir di posisi len-1
kemudian mengurangi nilai len
untuk memperbarui posisi puncak tumpukan sesuai.
Implementasi lengkap dari Stack, bersama dengan struktur data lain yang dapat Anda gunakan dalam kode Anda, dapat ditemukan dalam perpustakaan yang dijaga oleh komunitas Alexandria, di dalam crate "data_structures".
Ringkasan
Meskipun model memori Cairo bersifat tidak dapat diubah dan dapat membuat sulit untuk mengimplementasikan struktur data yang dapat diubah, kita untungnya dapat menggunakan tipe Felt252Dict<T>
untuk mensimulasikan struktur data yang dapat diubah. Ini memungkinkan kita untuk mengimplementasikan berbagai struktur data yang berguna untuk banyak aplikasi, efektif menyembunyikan kompleksitas model memori yang mendasarinya.
Memahami Sistem Kepemilikan Cairo
Cairo adalah bahasa yang dibangun di sekitar sistem tipe linear yang memungkinkan kita untuk memastikan secara statis bahwa dalam setiap program Cairo, sebuah nilai digunakan tepat satu kali. Sistem tipe linear ini membantu mencegah kesalahan saat runtime dengan memastikan bahwa operasi yang dapat menyebabkan kesalahan tersebut, seperti menulis dua kali ke sel memori, terdeteksi pada waktu kompilasi. Hal ini dicapai dengan mengimplementasikan sistem kepemilikan dan melarang penyalinan serta penghapusan nilai secara default. Dalam bab ini, kita akan membahas sistem kepemilikan Cairo serta referensi dan snapshot.
Pemilikan Menggunakan Sistem Tipe Linear
Cairo menggunakan sistem tipe linear. Dalam sistem tipe seperti ini, setiap nilai (tipe dasar, struktur data, enum) harus digunakan dan hanya boleh digunakan sekali. 'Digunakan' di sini berarti bahwa nilai tersebut entah dihancurkan atau dipindahkan.
Penjelasan dapat terjadi dalam beberapa cara:
- sebuah variabel keluar dari cakupan (out of scope)
- sebuah struktur data di-destrukturisasi
- penghancuran eksplisit menggunakan destruct()
Memindahkan nilai hanya berarti meneruskan nilai itu ke fungsi lain.
Hal ini menghasilkan batasan yang agak mirip dengan model kepemilikan Rust, tetapi ada beberapa perbedaan. Secara khusus, model kepemilikan Rust ada (sebagian) untuk menghindari perlombaan data (data races) dan akses mutabel konkuren ke nilai memori. Ini jelas tidak mungkin dalam Cairo karena memori bersifat tak berubah (immutable). Sebagai gantinya, Cairo memanfaatkan sistem tipe linear-nya untuk dua tujuan utama:
- Memastikan bahwa semua kode dapat dibuktikan (provable) dan dengan demikian dapat diverifikasi.
- Abstraksi memori yang tak berubah (immutable) dari VM Cairo.
Kepemilikan (Ownership)
Dalam Cairo, kepemilikan berlaku untuk variabel dan bukan untuk nilai (values). Sebuah nilai dapat dengan aman dirujuk oleh banyak variabel yang berbeda (bahkan jika mereka adalah variabel mutabel), karena nilai itu sendiri selalu tidak berubah (immutable). Namun variabel dapat bersifat mutabel, sehingga kompilator harus memastikan bahwa variabel konstan tidak secara tidak sengaja diubah oleh programmer. Hal ini membuat mungkin untuk berbicara tentang kepemilikan dari sebuah variabel: pemilik (owner) adalah kode yang dapat membaca (dan menulis jika mutabel) variabel tersebut.
Ini berarti bahwa variabel (bukan nilai) mengikuti aturan yang mirip dengan nilai-nilai Rust:
- Setiap variabel di Cairo memiliki seorang pemilik (owner).
- Hanya boleh ada satu pemilik pada satu waktu.
- Ketika pemilik keluar dari cakupan (out of scope), variabel itu dihancurkan.
Sekarang setelah kita melewati sintaks dasar Cairo, kita tidak akan menyertakan semua contoh fn main() {
di dalam fungsi main
secara manual. Sebagai hasilnya, contoh-contoh kita akan menjadi kode dalam contoh-contoh, jadi jika Anda mengikuti, pastikan untuk menambahkan sedikit yang berikut ini lebih ringkas, memungkinkan kita fokus pada detail sebenarnya daripada kode boilerplate.
Cakupan Variabel
Sebagai contoh pertama dari sistem tipe linear, kita akan melihat cakupan dari beberapa variabel. Cakupan adalah rentang dalam sebuah program di mana suatu item valid. Ambil contoh variabel berikut:
let s = 'hello';
Variabel s
merujuk pada string pendek. Variabel ini valid mulai dari titik deklarasinya hingga akhir cakupan (scope) saat ini. Listing 4-1 menunjukkan sebuah program dengan komentar yang menandai dimana variabel s
akan valid.
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing_03_01/src/lib.cairo:here}}
Listing 4-1: Sebuah variabel dan cakupan di mana variabel tersebut valid
Dengan kata lain, ada dua poin penting dalam waktu ini:
- Ketika
s
masuk ke dalam cakupan, itu valid. - Itu tetap valid hingga keluar dari cakupan.
Pada titik ini, hubungan antara cakupan dan kapan variabel valid serupa dengan dalam bahasa pemrograman lainnya. Sekarang kita akan membangun di atas pemahaman ini dengan menggunakan tipe Array
yang kita perkenalkan dalam bab sebelumnya.
Memindahkan Nilai - Contoh dengan Array
Seperti yang disebutkan sebelumnya, memindahkan nilai hanya berarti meneruskan nilai tersebut ke fungsi lain. Ketika itu terjadi, variabel yang merujuk pada nilai tersebut dalam cakupan asli dihancurkan dan tidak dapat lagi digunakan, dan variabel baru dibuat untuk menyimpan nilai yang sama.
Array adalah contoh tipe kompleks yang dipindahkan ketika dilewatkan ke fungsi lain. Berikut adalah pengingat singkat tentang tampilan array:
{{#rustdoc_include ../listings/ch04-understanding-ownership/no_listing_01_array/src/lib.cairo:2:4}}
Bagaimana sistem tipe memastikan bahwa program Cairo tidak pernah mencoba menulis ke sel memori yang sama dua kali? Pertimbangkan kode berikut, di mana kita mencoba untuk menghapus bagian depan array dua kali:
{{#include ../listings/ch04-understanding-ownership/no_listing_02_pass_array_by_value/src/lib.cairo}}
Dalam kasus ini, kita mencoba meneruskan nilai yang sama (array dalam variabel arr
) ke kedua panggilan fungsi. Ini berarti kode kita mencoba untuk menghapus elemen pertama dua kali, yang akan mencoba untuk menulis ke sel memori yang sama dua kali - yang dilarang oleh VM Cairo, menyebabkan kesalahan saat runtime.
Untungnya, kode ini sebenarnya tidak dikompilasi. Setelah kita meneruskan array ke fungsi foo
, variabel arr
tidak lagi dapat digunakan. Kita mendapatkan kesalahan pada waktu kompilasi ini, memberitahu kita bahwa kita perlu Array mengimplementasikan Trait Copy:
error: Variabel sebelumnya telah dipindahkan. Trait tidak memiliki implementasi dalam konteks: core::traits::Copy::<core::array::Array::<core::integer::u128>>
--> array.cairo:6:9
let mut arr = ArrayTrait::<u128>::new();
^*****^
Trait Copy
Jika suatu tipe mengimplementasikan trait Copy
, mempassing nilai dari tipe tersebut ke fungsi tidak akan memindahkan nilai tersebut. Sebagai gantinya, variabel baru dibuat, merujuk pada nilai yang sama.
Hal penting yang perlu dicatat di sini adalah bahwa ini adalah operasi yang benar-benar gratis, karena variabel hanyalah abstraksi Cairo dan karena nilai di Cairo selalu tidak berubah (immutable). Hal ini, khususnya, konseptual berbeda dari versi Rust dari trait Copy
, di mana nilai potensialnya disalin di memori.
Anda dapat mengimplementasikan trait Copy
pada tipe Anda dengan menambahkan anotasi #[derive(Copy)]
pada definisi tipe Anda. Namun, Cairo tidak akan mengizinkan tipe untuk dianotasi dengan Copy jika tipe itu sendiri atau salah satu komponennya tidak mengimplementasikan trait Copy.
Meskipun Array dan Dictionary tidak dapat disalin, tipe kustom yang tidak mengandung keduanya bisa.
{{#include ../listings/ch04-understanding-ownership/no_listing_03_copy_trait/src/lib.cairo}}
Dalam contoh ini, kita dapat meneruskan p1
dua kali ke fungsi foo karena tipe Point
mengimplementasikan trait Copy
. Ini berarti bahwa ketika kita meneruskan p1
ke foo
, kita sebenarnya meneruskan salinan dari p1
, sehingga p1
tetap valid. Dalam istilah kepemilikan, ini berarti bahwa kepemilikan p1
tetap berada pada fungsi utama.
Jika Anda menghapus turunan trait Copy
dari tipe Point
, Anda akan mendapatkan kesalahan waktu kompilasi saat mencoba mengompilasi kode tersebut.
Jangan khawatir tentang kata kunci Struct
. Kami akan memperkenalkannya dalam Bab 5.
Menghancurkan Nilai - Contoh dengan FeltDict
Cara lain tipe linear dapat digunakan adalah dengan dihancurkan. Penghancuran harus memastikan bahwa 'sumber daya' sekarang dilepaskan dengan benar. Dalam Rust misalnya, ini bisa menjadi menutup akses ke file, atau mengunci sebuah mutex.
Di Cairo, satu tipe yang memiliki perilaku seperti ini adalah Felt252Dict
. Untuk kepastian, dictionary harus 'disquash' ketika mereka dihancurkan.
Ini bisa sangat mudah dilupakan, jadi ini ditegakkan oleh sistem tipe dan kompilator.
Penghancuran yang tidak berpengaruh: Trait Drop
Anda mungkin telah memperhatikan bahwa tipe Point
pada contoh sebelumnya juga mengimplementasikan trait Drop
.
Misalnya, kode berikut tidak akan dikompilasi, karena struktur A
tidak dipindahkan atau dihancurkan sebelum keluar dari cakupan:
{{#include ../listings/ch04-understanding-ownership/no_listing_04_no_drop_derive_fails/src/lib.cairo}}
Namun, tipe yang mengimplementasikan trait Drop
secara otomatis dihancurkan saat keluar dari cakupan. Penghancuran ini tidak melakukan apa-apa, ini hanya sebuah hint kepada kompilator bahwa tipe ini dapat dengan aman dihancurkan begitu tidak lagi berguna. Kami menyebut ini "meng-drop" sebuah nilai.
Saat ini, implementasi Drop
dapat diturunkan untuk semua tipe, memungkinkan mereka di-drop saat keluar dari cakupan, kecuali untuk kamus (Felt252Dict
) dan tipe yang berisi kamus.
Sebagai contoh, kode berikut akan dikompilasi:
{{#include ../listings/ch04-understanding-ownership/no_listing_05_drop_derive_compiles/src/lib.cairo}}
Penghancuran dengan Efek Samping: trait Destruct
Ketika sebuah nilai dihancurkan, kompilator pertama-tama mencoba memanggil metode drop
pada tipe tersebut. Jika tidak ada, maka kompilator mencoba memanggil destruct
sebagai gantinya. Metode ini disediakan oleh trait Destruct
.
Seperti yang disebutkan sebelumnya, kamus (dictionaries) di Cairo adalah tipe yang harus "disquash" saat dihancurkan, sehingga urutan akses dapat dibuktikan. Hal ini mudah dilupakan oleh para pengembang, jadi sebagai gantinya kamus mengimplementasikan trait Destruct
untuk memastikan bahwa semua kamus disquash ketika keluar dari cakupan.
Dengan demikian, contoh berikut tidak akan dikompilasi:
{{#include ../listings/ch04-understanding-ownership/no_listing_06_no_destruct_compile_fails/src/lib.cairo}}
Jika Anda mencoba menjalankan kode ini, Anda akan mendapatkan kesalahan waktu kompilasi:
error: Variable not dropped. Trait has no implementation in context: core::traits::Drop::<temp7::temp7::A>. Trait has no implementation in context: core::traits::Destruct::<temp7::temp7::A>.
--> temp7.cairo:7:5
A {
^*^
Ketika A
keluar dari cakupan, itu tidak dapat di-drop karena tidak mengimplementasikan trait Drop
(karena berisi kamus dan tidak bisa derive(Drop)
) maupun trait Destruct
. Untuk memperbaiki ini, kita dapat menurunkan implementasi trait Destruct
untuk tipe A
:
{{#include ../listings/ch04-understanding-ownership/no_listing_07_destruct_compiles/src/lib.cairo}}
Sekarang, ketika A
keluar dari cakupan, kamusnya akan secara otomatis disquash, dan program akan berhasil dikompilasi.
Menyalin Data Array dengan Clone
Jika kita ingin menyalin data dari sebuah Array
, kita dapat menggunakan metode umum yang disebut clone
. Kita akan membahas sintaks metode dalam Bab 6, tetapi karena metode adalah fitur umum dalam banyak bahasa pemrograman, Anda mungkin sudah pernah melihatnya sebelumnya.
Berikut adalah contoh dari metode clone
dalam aksi.
{{#include ../listings/ch04-understanding-ownership/no_listing_08_array_clone/src/lib.cairo}}
Ketika Anda melihat panggilan ke clone
, Anda tahu bahwa ada beberapa kode sembarang yang sedang dieksekusi dan kode tersebut mungkin mahal. Itu merupakan indikator visual bahwa ada sesuatu yang berbeda terjadi.
Dalam kasus ini, nilai sedang disalin, menghasilkan penggunaan sel memori baru, dan variabel baru diciptakan, merujuk pada nilai yang baru, disalin.
Nilai Kembali dan Cakupan
Mengembalikan nilai setara dengan memindahkan mereka. Listing 4-4 menunjukkan contoh sebuah fungsi yang mengembalikan beberapa nilai, dengan anotasi serupa seperti pada Listing 4-3.
Nama File: src/lib.cairo
{{#include ../listings/ch04-understanding-ownership/listing_03_04/src/lib.cairo}}
Listing 4-4: Memindahkan nilai kembali
Meskipun ini berfungsi, memindahkan masuk dan keluar dari setiap fungsi agak membosankan. Bagaimana jika kita ingin membiarkan sebuah fungsi menggunakan nilai tetapi tidak memindahkan nilainya? Sangat menjengkelkan bahwa apa pun yang kita lewatkan juga perlu dilewatkan kembali jika kita ingin menggunakannya lagi, selain dari data apapun yang dihasilkan dari tubuh fungsi yang mungkin ingin kita kembalikan juga.
Cairo memungkinkan kita untuk mengembalikan beberapa nilai menggunakan tuple, seperti yang ditunjukkan dalam Listing 4-5.
Nama File: src/lib.cairo
{{#include ../listings/ch04-understanding-ownership/listing_03_05/src/lib.cairo}}
Listing 4-5: Mengembalikan banyak nilai
Tetapi ini terlalu banyak cakupan dan banyak pekerjaan untuk konsep yang seharusnya umum. Untungnya untuk kita, Cairo memiliki dua fitur untuk melewatkan nilai tanpa menghancurkannya atau memindahkannya, disebut referensi dan snapshot.
Referensi dan Cuplikan
Permasalahan pada kode tuple dalam Listing 4-5 adalah kita harus mengembalikan Array
ke fungsi pemanggil sehingga kita masih bisa menggunakan Array
setelah pemanggilan ke calculate_length
, karena Array
telah dipindahkan ke dalam calculate_length
.
Cuplikan
Pada bab sebelumnya, kita membahas bagaimana sistem kepemilikan Cairo mencegah kita menggunakan variabel setelah kita telah memindahkannya, melindungi kita dari potensi menulis dua kali ke sel memori yang sama. Namun, ini tidak begitu nyaman. Mari kita lihat bagaimana kita dapat mempertahankan kepemilikan variabel dalam fungsi pemanggil dengan menggunakan snapshot.
Di Cairo, snapshot adalah tampilan tidak berubah dari suatu nilai pada titik waktu tertentu. Ingatlah bahwa memori tidak berubah, sehingga memodifikasi nilai sebenarnya membuat sel memori baru. Sel memori lama masih ada, dan snapshot adalah variabel yang merujuk pada nilai "lama" tersebut. Dalam hal ini, snapshot adalah tampilan "ke masa lalu".
Berikut adalah bagaimana Anda akan mendefinisikan dan menggunakan fungsi calculate_length
yang mengambil snapshot ke array sebagai parameter daripada mengambil kepemilikan dari nilai yang mendasarinya. Pada contoh ini, fungsi calculate_length
mengembalikan panjang array yang dilewatkan sebagai parameter. Karena kita melewatinya sebagai snapshot, yang merupakan tampilan tidak berubah dari array, kita dapat yakin bahwa fungsi calculate_length
tidak akan mengubah array, dan kepemilikan array tetap ada di dalam fungsi utama.
Nama file: src/lib.cairo
{{#include ../listings/ch04-understanding-ownership/no_listing_09_snapshots/src/lib.cairo}}
Catatan: Hanya mungkin untuk memanggil metode
len()
pada snapshot array karena itu didefinisikan seperti itu dalam traitArrayTrait
. Jika Anda mencoba memanggil metode yang tidak didefinisikan untuk snapshot pada snapshot, Anda akan mendapatkan kesalahan kompilasi. Namun, Anda dapat memanggil metode yang mengharapkan snapshot pada tipe non-snapshot.
Keluaran dari program ini adalah:
[DEBUG] (raw: 0)
[DEBUG] (raw: 1)
Pengembalian berhasil, mengembalikan []
Pertama, perhatikan bahwa semua kode tuple dalam deklarasi variabel dan nilai kembalian fungsi telah hilang. Kedua, perhatikan bahwa kita melewatkan @arr1
ke calculate_length
dan, dalam definisinya, kita menggunakan @Array<u128>
daripada Array<u128>
.
Mari kita perhatikan lebih dekat panggilan fungsi di sini:
{{#rustdoc_include ../listings/ch04-understanding-ownership/no_listing_09_snapshots/src/lib.cairo:function_call}}
Sintaks @arr1
memungkinkan kita membuat snapshot dari nilai dalam arr1
. Karena snapshot adalah tampilan tidak berubah dari nilai pada titik waktu tertentu, aturan biasa dari sistem tipe linear tidak diberlakukan. Khususnya, variabel snapshot selalu Drop
, tidak pernah Destruct
, bahkan snapshot kamus.
Sama halnya, tanda fungsi menggunakan @
untuk menunjukkan bahwa tipe dari parameter arr
adalah snapshot. Mari tambahkan beberapa anotasi penjelas:
fn calculate_length(
array_snapshot: @Array<u128>
) -> usize { // array_snapshot adalah snapshot dari Array
array_snapshot.len()
} // Di sini, array_snapshot keluar dari cakupan dan di-drop.
// Namun, karena ini hanya merupakan tampilan dari apa yang asli array `arr` berisi, `arr` asli masih bisa digunakan.
Cakupan di mana variabel array_snapshot
valid adalah sama dengan cakupan parameter fungsi apa pun, tetapi nilai mendasar dari snapshot tidak di-drop saat array_snapshot
berhenti digunakan. Ketika fungsi memiliki snapshot sebagai parameter alih-alih nilai sebenarnya, kita tidak perlu mengembalikan nilai tersebut untuk memberikan kepemilikan kembali dari nilai asli, karena kita tidak pernah memiliki kepemilikannya.
Operator Desnap
Untuk mengonversi snapshot kembali ke variabel reguler, Anda dapat menggunakan operator desnap
*
, yang berfungsi sebagai kebalikan dari operator @
.
Hanya tipe Copy
yang dapat didesnap. Namun, dalam kasus umum, karena nilai tidak dimodifikasi, variabel baru yang dibuat oleh operator desnap
menggunakan kembali nilai lama, sehingga proses desnapping adalah operasi yang sepenuhnya gratis, sama seperti Copy
.
Pada contoh berikut, kita ingin menghitung luas persegi panjang, tetapi kita tidak ingin mengambil kepemilikan persegi panjang dalam fungsi calculate_area
, karena kita mungkin ingin menggunakan persegi panjang tersebut lagi setelah pemanggilan fungsi. Karena fungsi kita tidak mengubah instance persegi panjang, kita dapat melewatkan snapshot dari persegi panjang ke fungsi, dan kemudian mengubah snapshot kembali menjadi nilai menggunakan operator desnap
*
.
{{#include ../listings/ch04-understanding-ownership/no_listing_10_desnap/src/lib.cairo}}
Namun, apa yang terjadi jika kita mencoba memodifikasi sesuatu yang kita lewati sebagai snapshot? Cobalah kode pada Listing 4-6. SPOILER: Ini tidak berhasil!
Nama file: src/lib.cairo
{{#include ../listings/ch04-understanding-ownership/listing_03_06/src/lib.cairo}}
Listing 4-6: Mencoba memodifikasi nilai snapshot
Berikut adalah kesalahannya:
error: Invalid left-hand side of assignment.
--> ownership.cairo:15:5
rec.height = rec.width;
^********^
Kompilator mencegah kita untuk memodifikasi nilai yang terkait dengan snapshot.
Referensi Mutable
Kita bisa mencapai perilaku yang kita inginkan pada Listing 4-6 dengan menggunakan referensi mutable alih-alih snapshot. Referensi mutable sebenarnya adalah nilai yang dapat diubah yang dilewatkan ke fungsi yang secara implisit dikembalikan pada akhir fungsi, mengembalikan kepemilikan ke konteks pemanggil. Dengan cara ini, mereka memungkinkan Anda untuk mengubah nilai yang dilewatkan sambil tetap memegang kepemilikannya dengan mengembalikannya secara otomatis pada akhir eksekusi.
Di Cairo, sebuah parameter dapat dilewati sebagai referensi mutable menggunakan modifier ref
.
Catatan: Di Cairo, sebuah parameter hanya dapat dilewati sebagai referensi mutable menggunakan modifier
ref
jika variabel dideklarasikan sebagai mutable denganmut
.
Pada Listing 4-7, kita menggunakan referensi mutable untuk mengubah nilai bidang height
dan width
dari instan Rectangle
dalam fungsi flip
.
{{#include ../listings/ch04-understanding-ownership/listing_03_07/src/lib.cairo}}
Listing 4-7: Penggunaan referensi mutable untuk mengubah nilai
Pertama, kita ubah rec
menjadi mut
. Kemudian kita lewatkan referensi mutable dari rec
ke flip
dengan ref rec
, dan perbarui tanda tangan fungsi untuk menerima referensi mutable dengan ref rec: Rectangle
. Ini membuatnya sangat jelas bahwa fungsi flip
akan mengubah nilai dari instan Rectangle
yang dilewatkan sebagai parameter.
Keluaran dari program ini adalah:
[DEBUG]
(raw: 10)
[DEBUG] (raw: 3)
Seperti yang diharapkan, bidang height
dan width
dari variabel rec
telah ditukar.
Ringkasan Kecil
Mari kita ringkas apa yang telah kita diskusikan tentang sistem tipe linear, kepemilikan, snapshot, dan referensi:
- Pada suatu waktu tertentu, sebuah variabel hanya dapat dimiliki oleh satu pemilik.
- Anda dapat melewati sebuah variabel berdasarkan nilai, snapshot, atau referensi ke fungsi.
- Jika Anda melewatinya berdasarkan nilai, kepemilikan variabel dipindahkan ke fungsi.
- Jika Anda ingin tetap memegang kepemilikan variabel dan tahu bahwa fungsi Anda tidak akan memutasi variabel tersebut, Anda dapat melewatinya sebagai snapshot dengan
@
. - Jika Anda ingin tetap memegang kepemilikan variabel dan tahu bahwa fungsi Anda akan memutasi variabel tersebut, Anda dapat melewatinya sebagai referensi mutable dengan
ref
.
Menggunakan Struct untuk Mengatur Data yang Berhubungan
Sebuah struct, atau struktur, adalah tipe data kustom yang memungkinkan Anda mengemas bersama dan memberi nama pada beberapa nilai yang berhubungan yang membentuk kelompok yang bermakna. Jika Anda akrab dengan bahasa pemrograman berorientasi objek, sebuah struct mirip dengan atribut data dari sebuah objek. Pada bab ini, kita akan membandingkan dan membedakan tuple dengan struct untuk memperluas pengetahuan yang sudah Anda miliki dan menunjukkan kapan struct merupakan cara yang lebih baik untuk mengelompokkan data.
Kita akan menunjukkan bagaimana cara mendefinisikan dan membuat instance dari struct. Kita akan membahas cara mendefinisikan fungsi terkait, terutama jenis fungsi terkait yang disebut metode, untuk menentukan perilaku yang terkait dengan tipe struct. Struct dan enumerasi (dibahas pada bab selanjutnya) adalah blok bangunan untuk membuat tipe-tipe baru dalam domain program Anda agar dapat memanfaatkan penuh pengecekan tipe pada saat kompilasi Cairo.
Mendefinisikan dan Membuat Instance Struct
Struct mirip dengan tuple, yang dibahas dalam bagian Tipe Data, karena keduanya menyimpan beberapa nilai yang berhubungan. Seperti tuple, bagian-bagian dari sebuah struct dapat memiliki tipe data yang berbeda. Berbeda dengan tuple, pada struct, Anda akan memberikan nama pada setiap bagian data sehingga jelas apa makna dari nilai-nilai tersebut. Menambahkan nama-nama ini berarti bahwa struct lebih fleksibel daripada tuple: Anda tidak harus bergantung pada urutan data untuk menentukan atau mengakses nilai dari sebuah instance.
Untuk mendefinisikan sebuah struct, kita memasukkan kata kunci struct
dan memberi nama pada seluruh struct. Nama dari sebuah struct seharusnya menjelaskan pentingnya bagian-bagian data yang dikelompokkan bersama. Kemudian, di dalam kurung kurawal, kita mendefinisikan nama dan tipe data dari setiap bagian data, yang disebut sebagai fields. Sebagai contoh, Listing 5-1 menunjukkan sebuah struct yang menyimpan informasi tentang akun pengguna.
Nama File: src/lib.cairo
{{#include ../listings/ch05-using-structs-to-structure-related-data/listing_04_01_user_struct/src/lib.cairo:user}}
Listing 5-1: Definisi struct User
Untuk menggunakan sebuah struct setelah kita mendefinisikannya, kita membuat sebuah instance dari struct tersebut dengan menentukan nilai konkret untuk setiap field. Kita membuat sebuah instance dengan menyebutkan nama struct dan kemudian menambahkan kurung kurawal yang berisi pasangan key: value, di mana kunci adalah nama-nama dari fields dan nilai adalah data yang ingin kita simpan pada fields tersebut. Kita tidak harus menyebutkan fields dalam urutan yang sama dengan yang kita deklarasikan pada struct. Dengan kata lain, definisi struct adalah seperti template umum untuk tipe tersebut, dan instance mengisi template tersebut dengan data tertentu untuk membuat nilai dari tipe tersebut.
Sebagai contoh, kita dapat mendeklarasikan seorang pengguna tertentu seperti yang ditunjukkan pada Listing 5-2.
Nama File: src/lib.cairo
{{#include ../listings/ch05-using-structs-to-structure-related-data/listing_04_01_user_struct/src/lib.cairo:all}}
Listing 5-2: Membuat sebuah instance dari struct User
Untuk mendapatkan nilai spesifik dari sebuah struct, kita menggunakan notasi titik (dot notation). Sebagai contoh, untuk mengakses Address email pengguna ini, kita menggunakan user1.email
. Jika instance bersifat mutable, kita dapat mengubah nilai dengan menggunakan notasi titik dan mengassign ke field tertentu. Listing 5-3 menunjukkan bagaimana cara mengubah nilai pada field email
dari sebuah instance User
yang mutable.
Nama File: src/lib.cairo
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing_04_03_mut_struct/src/lib.cairo:main}}
Listing 5-3: Mengubah nilai pada field email dari sebuah instance User
Perhatikan bahwa seluruh instance harus bersifat mutable; Cairo tidak mengizinkan kita untuk menandai hanya beberapa field tertentu sebagai mutable.
Seperti halnya dengan ekspresi lainnya, kita dapat membuat sebuah instance baru dari struct sebagai ekspresi terakhir dalam tubuh fungsi untuk secara implisit mengembalikan instance baru tersebut.
Listing 5-4 menunjukkan sebuah fungsi build_user
yang mengembalikan sebuah instance User
dengan email dan username yang diberikan. Field active
mendapatkan nilai true
, dan field sign_in_count
mendapatkan nilai 1
.
Nama File: src/lib.cairo
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing_04_03_mut_struct/src/lib.cairo:build_user}}
Listing 5-4: Sebuah fungsi build_user
yang mengambil email dan username dan mengembalikan sebuah instance User
Masuk akal untuk memberi nama parameter fungsi dengan nama yang sama dengan fields struct, namun harus mengulang nama-nama email
dan username
serta variabel adalah sedikit membosankan. Jika struct memiliki lebih banyak fields, mengulangi setiap nama akan menjadi lebih menjengkelkan. Untungnya, ada singkatan yang nyaman!
Menggunakan Singkatan Inisialisasi Field (Field Init Shorthand)
Karena nama parameter dan nama field struct sama persis dalam Listing 5-4, kita dapat menggunakan sintaks singkatan inisialisasi field untuk menulis ulang build_user
agar berperilaku sama namun tanpa pengulangan username
dan email
, seperti yang ditunjukkan pada Listing 5-5.
Nama File: src/lib.cairo
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing_04_03_mut_struct/src/lib.cairo:build_user2}}
Listing 5-5: Sebuah fungsi build_user
yang menggunakan singkatan inisialisasi field karena parameter username
dan email
memiliki nama yang sama dengan fields struct
Di sini, kita membuat sebuah instance baru dari struct User
, yang memiliki field bernama email
. Kita ingin mengatur nilai field email
ke nilai pada parameter email
dari fungsi build_user
. Karena field email
dan parameter email
memiliki nama yang sama, kita hanya perlu menuliskan email
daripada email: email
.
Contoh Program Menggunakan Structs
Untuk memahami kapan kita sebaiknya menggunakan structs, mari kita tulis sebuah program yang menghitung luas dari sebuah persegi panjang. Kita akan mulai dengan menggunakan variabel tunggal, dan kemudian merefactor program tersebut hingga kita menggunakan structs.
Mari buat sebuah proyek baru dengan Scarb yang disebut rectangles yang akan mengambil lebar dan tinggi dari sebuah persegi panjang yang ditentukan dalam piksel dan menghitung luas dari persegi panjang tersebut. Listing 5-6 menunjukkan sebuah program pendek dengan salah satu cara untuk melakukan hal tersebut dalam proyek kita pada src/lib.cairo.
Nama File: src/lib.cairo
{{#include ../listings/ch05-using-structs-to-structure-related-data/listing_04_06_no_struct/src/lib.cairo:all}}
Listing 5-6: Menghitung luas dari sebuah persegi panjang yang ditentukan oleh variabel lebar dan tinggi terpisah
Sekarang jalankan program dengan scarb cairo-run --available-gas=200000000
:
$ scarb cairo-run --available-gas=200000000
[DEBUG] , (raw: 300)
Run completed successfully, returning []
Kode ini berhasil menghitung luas dari persegi panjang dengan memanggil fungsi area
dengan setiap dimensi, tetapi kita dapat melakukan lebih banyak lagi untuk membuat kode ini lebih jelas dan mudah dibaca.
Permasalahan dengan kode ini terlihat pada tanda tangan dari area
:
{{#include ../listings/ch05-using-structs-to-structure-related-data/listing_04_06_no_struct/src/lib.cairo:here}}
Fungsi area
seharusnya menghitung luas dari satu persegi panjang, namun fungsi yang kita tulis memiliki dua parameter, dan tidak jelas di mana pun dalam program kita bahwa parameter tersebut saling terkait. Akan lebih mudah dibaca dan dikelola jika kita mengelompokkan lebar dan tinggi bersama-sama. Kami telah membahas satu cara yang mungkin kita lakukan dalam Bab 2: menggunakan tuples.
Merefactor dengan Tuples
Listing 5-7 menunjukkan versi lain dari program kita yang menggunakan tuples.
Nama File: src/lib.cairo
{{#include ../listings/ch05-using-structs-to-structure-related-data/listing_04_07_w_tuples/src/lib.cairo}}
Listing 5-7: Menentukan lebar dan tinggi dari persegi panjang dengan sebuah tuple
Dalam satu aspek, program ini lebih baik. Tuples memungkinkan kita untuk menambah sedikit struktur, dan sekarang kita hanya melewatkan satu argumen. Namun dalam aspek lain, versi ini kurang jelas: tuples tidak memberi nama pada elemennya, sehingga kita harus mengindeks ke bagian-bagian dari tuple, membuat perhitungan kita kurang jelas.
Membingungkan lebar dan tinggi tidak akan masalah untuk perhitungan luas, tetapi jika kita ingin menghitung perbedaannya, itu akan masalah! Kita harus ingat bahwa lebar
adalah indeks tuple 0
dan tinggi
adalah indeks tuple 1
. Ini akan menjadi lebih sulit bagi orang lain untuk memahami dan diingat jika mereka menggunakan kode kita. Karena kita tidak menyampaikan makna dari data kita dalam kode kita, sekarang lebih mudah untuk memperkenalkan kesalahan.
Merefactor dengan Structs: Menambah Makna Lebih Banyak
Kita menggunakan structs untuk menambah makna dengan memberi label pada data tersebut. Kita dapat mengubah tuple yang kita gunakan menjadi sebuah struct dengan nama untuk keseluruhannya serta nama untuk bagian-bagiannya.
Nama File: src/lib.cairo
{{#include ../listings/ch05-using-structs-to-structure-related-data/listing_04_08_w_structs/src/lib.cairo}}
Listing 5-8: Mendefinisikan sebuah struct Rectangle
Di sini kita telah mendefinisikan sebuah struct dan memberinya nama Rectangle
. Di dalam kurung kurawal, kita mendefinisikan fields sebagai lebar
dan tinggi
, kedua-duanya memiliki tipe u64
. Kemudian, di main
, kita membuat sebuah instance tertentu dari Rectangle
yang memiliki lebar 30
dan tinggi 10
. Fungsi area
kita sekarang didefinisikan dengan satu parameter, yang kita namakan persegiPanjang
yang merupakan tipe struct Rectangle
. Kita dapat mengakses fields dari instance dengan notasi titik, dan ini memberikan nama yang deskriptif pada nilai-nilai tersebut daripada menggunakan nilai indeks tuple 0
dan 1
.
Menambah Fungsionalitas Berguna dengan Trait
Akan berguna untuk dapat mencetak sebuah instance dari Rectangle
saat kita sedang melakukan debugging pada program kita dan melihat nilai-nilai untuk semua fieldsnya. Listing 5-9 mencoba menggunakan print
seperti yang telah kita gunakan pada bab-bab sebelumnya. Ini tidak akan berhasil.
Nama File: src/lib.cairo
{{#include ../listings/ch05-using-structs-to-structure-related-data/listing_04_10_print_rectangle/src/lib.cairo:here}}
Listing 5-9: Mencoba untuk mencetak sebuah instance Rectangle
Ketika kita mengompilasi kode ini, kita mendapatkan sebuah error dengan pesan ini:
$ cairo-compile src/lib.cairo
error: Method `print` not found on type "../src::Rectangle". Did you import the correct trait and impl?
--> lib.cairo:16:15
rectangle.print();
^***^
Error: Compilation failed.
Trait print
diimplementasikan untuk banyak tipe data, tetapi tidak untuk struct Rectangle
. Kita dapat memperbaikinya dengan mengimplementasikan trait PrintTrait
pada Rectangle
seperti yang ditunjukkan pada Listing 5-10.
Untuk mempelajari lebih lanjut tentang traits, lihat Traits in Cairo.
Nama File: src/lib.cairo
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing_04_10_print_rectangle/src/lib.cairo:all}}
Listing 5-10: Mengimplementasikan trait PrintTrait
pada Rectangle
Bagus! Ini bukanlah output yang paling cantik, tetapi ini menunjukkan nilai-nilai dari semua fields untuk instance ini, yang pasti akan membantu saat debugging.
Sintaks Metode
Metode mirip dengan fungsi: kita mendeklarasikannya dengan kata kunci fn
dan sebuah nama, mereka dapat memiliki parameter dan nilai kembali, dan mereka berisi kode yang dijalankan saat metode tersebut dipanggil dari tempat lain. Berbeda dengan fungsi, metode didefinisikan dalam konteks suatu tipe dan parameter pertamanya selalu self
, yang mewakili instansi dari tipe yang metode itu dipanggil. Bagi yang akrab dengan Rust, pendekatan Cairo mungkin membingungkan, karena metode tidak dapat didefinisikan langsung pada tipe. Sebagai gantinya, Anda harus mendefinisikan sebuah trait dan implementasi yang terkait dengan tipe yang dimaksudkan untuk metode tersebut.
Mendefinisikan Metode
Mari ubah fungsi area
yang memiliki sebuah instansi Rectangle
sebagai parameter menjadi sebuah metode area
yang didefinisikan pada trait RectangleTrait
, seperti yang ditunjukkan pada Listing 5-13.
Nama File: src/lib.cairo
{{#include ../listings/ch05-using-structs-to-structure-related-data/listing_04_13_area_method/src/lib.cairo}}
Listing 5-13: Mendefinisikan metode area
untuk digunakan pada Rectangle
Untuk mendefinisikan fungsi dalam konteks Rectangle
, kita mulai dengan mendefinisikan blok trait
dengan tanda tangan dari metode yang ingin kita implementasikan. Trait tidak terkait dengan tipe spesifik; hanya parameter self
dari metode yang menentukan tipe mana yang bisa digunakan dengannya. Kemudian, kita mendefinisikan blok impl
(implementasi) untuk RectangleTrait
, yang mendefinisikan perilaku dari metode yang diimplementasikan. Segala sesuatu dalam blok impl
ini akan terkait dengan tipe dari parameter self
dari metode yang dipanggil. Meskipun teknisnya memungkinkan untuk mendefinisikan metode untuk banyak tipe dalam satu blok impl
, ini bukanlah praktik yang disarankan, karena dapat menyebabkan kebingungan. Kami menyarankan agar tipe dari parameter self
tetap konsisten dalam satu blok impl
. Kemudian, kita memindahkan fungsi area
ke dalam kurung kurawal impl
dan mengubah parameter pertama (dan dalam kasus ini, hanya satu) menjadi self
dalam tanda tangan dan di semua bagian dalam badan fungsi. Di main
, di mana kita memanggil fungsi area
dan meneruskan rect1
sebagai argumen, kita dapat menggunakan syntax metode untuk memanggil metode area
pada instansi Rectangle
kita. Syntax metode digunakan setelah sebuah instansi: kita tambahkan titik diikuti oleh nama metode, tanda kurung, dan argumen apapun.
Metode harus memiliki parameter bernama self
dari tipe yang akan diterapkan untuk parameter pertamanya. Perhatikan bahwa kami menggunakan operator @
di depan tipe Rectangle
pada tanda tangan fungsi. Dengan melakukannya, kami menunjukkan bahwa metode ini mengambil snapshot yang tidak dapat diubah dari instansi Rectangle
, yang otomatis dibuat oleh kompiler saat melewatkan instansi ke metode. Metode dapat memiliki kepemilikan dari self
, menggunakan self
dengan snapshot seperti yang telah kita lakukan di sini, atau menggunakan referensi yang dapat diubah dari self
menggunakan sintaks ref self: T
.
Kami memilih self: @Rectangle
di sini dengan alasan yang sama kami menggunakan @Rectangle
pada versi fungsi: kami tidak ingin memiliki kepemilikan, dan kami hanya ingin membaca data dalam struct, bukan menulisnya. Jika kami ingin mengubah instansi yang telah kita panggil metode tersebut sebagai bagian dari apa yang dilakukan metode, kami akan menggunakan ref self: Rectangle
sebagai parameter pertama. Memiliki sebuah metode yang memiliki kepemilikan dari instansi dengan hanya menggunakan self
sebagai parameter pertama adalah hal yang jarang terjadi; teknik ini biasanya digunakan saat metode mengubah self
menjadi sesuatu yang lain dan Anda ingin mencegah pemanggil untuk menggunakan instansi asli setelah transformasi tersebut.
Perhatikan penggunaan operator desnap *
dalam metode area saat mengakses member dari struct. Ini diperlukan karena struct tersebut dilewatkan sebagai snapshot, dan semua nilai field-nya memiliki tipe @T
, yang mengharuskan untuk didesnap agar bisa dimanipulasi.
Alasan utama menggunakan metode daripada fungsi adalah untuk organisasi dan kejelasan kode. Kami telah menempatkan semua hal yang dapat kita lakukan dengan sebuah instansi dari suatu tipe dalam satu kombinasi blok trait
& impl
, daripada membuat pengguna masa depan dari kode kita mencari kemampuan dari Rectangle
di berbagai tempat dalam pustaka yang kami sediakan. Namun, kita dapat mendefinisikan beberapa kombinasi blok trait
& impl
untuk tipe yang sama di tempat yang berbeda, yang dapat berguna untuk organisasi kode yang lebih terperinci. Sebagai contoh, Anda bisa mengimplementasikan trait Add
untuk tipe Anda dalam satu blok impl
, dan trait Sub
dalam blok lainnya.
Catatan bahwa kita dapat memilih memberikan nama metode sama dengan salah satu field dari struct tersebut. Sebagai contoh, kita dapat mendefinisikan sebuah metode pada Rectangle
yang juga diberi nama width
:
Nama File: src/lib.cairo
{{#include ../listings/ch05-using-structs-to-structure-related-data/listing_04_14_width_method/src/lib.cairo}}
Di sini, kita memilih untuk membuat metode width
mengembalikan true
jika nilai dalam field width
dari instansi tersebut lebih besar dari 0
dan false
jika nilainya adalah 0
: kita dapat menggunakan field dalam sebuah metode dengan nama yang sama untuk tujuan apa pun. Di main
, ketika kita menggunakan rect1.width
dengan tanda kurung, Cairo mengetahui bahwa yang dimaksud adalah metode width
. Ketika kita tidak menggunakan tanda kurung, Cairo tahu bahwa yang dimaksud adalah field width
.
Metode dengan Lebih Banyak Parameter
Mari kita latihan menggunakan metode dengan mengimplementasikan sebuah metode kedua pada struct Rectangle
. Kali ini kita ingin sebuah instansi dari Rectangle
untuk menerima instansi lain dari Rectangle
dan mengembalikan true
jika Rectangle
kedua dapat benar-benar masuk dalam self
(Rectangle pertama); jika tidak, maka seharusnya mengembalikan false
. Artinya, setelah kita mendefinisikan metode can_hold
, kita ingin dapat menulis program yang ditunjukkan pada Listing 5-14.
Nama File: src/lib.cairo
{{#include ../listings/ch05-using-structs-to-structure-related-data/listing_04_15_can_hold/src/lib.cairo:no_method}}
Listing 5-14: Menggunakan metode can_hold
yang belum ditulis
Output yang diharapkan akan terlihat seperti berikut karena kedua dimensi dari rect2
lebih kecil dari dimensi dari rect1
, tetapi rect3
lebih lebar dari rect1
:
$ scarb cairo-run --available-gas=200000000
[DEBUG] Can rec1 hold rect2? (raw: 384675147322001379018464490539350216396261044799)
[DEBUG] true (raw: 1953658213)
[DEBUG] Can rect1 hold rect3? (raw: 384675147322001384331925548502381811111693612095)
[DEBUG] false (raw: 439721161573)
Kita tahu kita ingin mendefinisikan sebuah metode, sehingga metodenya akan berada dalam blok trait RectangleTrait
dan impl RectangleImpl of RectangleTrait
. Nama metodenya akan menjadi can_hold
, dan akan menerima sebuah snapshot dari Rectangle
lain sebagai parameter. Kita dapat mengetahui tipe dari parameter tersebut dengan melihat kode yang memanggil metodenya: rect1.can_hold(@rect2)
meneruskan @rect2
, yang merupakan snapshot ke rect2
, sebuah instansi dari Rectangle
. Hal ini masuk akal karena kita hanya perlu membaca rect2
(daripada menulis, yang berarti kita akan memerlukan pinjaman yang dapat diubah), dan kita ingin main
mempertahankan kepemilikan dari rect2
sehingga kita dapat menggunakannya kembali setelah memanggil metode can_hold
. Nilai kembalian dari can_hold
akan berupa Boolean, dan implementasinya akan memeriksa apakah lebar dan tinggi dari self
lebih besar dari lebar dan tinggi dari Rectangle
lain, secara berturut-turut. Mari tambahkan metode can_hold
baru ke dalam blok trait
dan impl
dari Listing 5-13, seperti yang ditunjukkan pada Listing 5-15.
Nama File: src/lib.cairo
{{#include ../listings/ch05-using-structs-to-structure-related-data/listing_04_15_can_hold/src/lib.cairo:trait_impl}}
Listing 5-15: Mengimplementasikan metode can_hold
pada Rectangle
yang menerima instansi Rectangle
lain sebagai parameter
Ketika kita menjalankan kode ini dengan fungsi main
pada Listing 5-14, kita akan mendapatkan output yang diinginkan. Metode dapat memiliki beberapa parameter yang kita tambahkan ke dalam tanda tangan setelah parameter self
, dan parameter-parameter tersebut berfungsi seperti parameter dalam fungsi.
Mengakses fungsi implementasi
Semua fungsi yang didefinisikan dalam blok trait
dan impl
dapat diakses langsung menggunakan operator ::
pada nama implementasi. Fungsi-fungsi dalam trait yang bukan merupakan metode sering digunakan untuk konstruktor yang akan mengembalikan instansi baru dari struct. Biasanya ini disebut new
, tetapi new
bukanlah nama khusus dan bukan bagian dari bahasa. Sebagai contoh, kita bisa memilih untuk menyediakan fungsi terkait yang dinamai square
yang akan memiliki satu parameter dimensi dan menggunakan nilai tersebut sebagai lebar dan tinggi, sehingga memudahkan pembuatan Rectangle
persegi daripada harus menentukan nilai yang sama dua kali:
Nama File: src/lib.cairo
{{#include ../listings/ch05-using-structs-to-structure-related-data/no_listing_01_implementation_functions/src/lib.cairo:here}}
Untuk memanggil fungsi ini, kita menggunakan sintaks ::
dengan nama implementasi; let square = RectangleImpl::square(10);
adalah contohnya. Fungsi ini berada di dalam ruang nama oleh implementasi; sintaks ::
digunakan baik untuk fungsi trait maupun ruang nama yang dibuat oleh modul. Kita akan membahas modul dalam [Chapter 8][modules].
Catatan: Juga memungkinkan untuk memanggil fungsi ini menggunakan nama trait, dengan
RectangleTrait::square(10)
.
Beberapa Blok impl
Setiap struct diizinkan memiliki beberapa blok trait
dan impl
. Sebagai contoh, Listing 5-15 setara dengan kode yang ditunjukkan pada Listing 5-16, di mana setiap metode berada di dalam blok trait
dan impl
yang terpisah.
{{#include ../listings/ch05-using-structs-to-structure-related-data/no_listing_02_multiple_impls/src/lib.cairo:here}}
Listing 5-16: Menulis ulang Listing 5-15 menggunakan beberapa blok impl
Tidak ada alasan untuk memisahkan metode-metode ini ke dalam beberapa blok trait
dan impl
di sini, namun ini adalah sintaks yang valid. Kita akan melihat kasus di mana blok-blok multiple berguna dalam Chapter 8, di mana kita membahas tipe-tipe generic dan trait.
Ringkasan
Struct memungkinkan Anda membuat tipe kustom yang bermakna untuk domain Anda. Dengan menggunakan struct, Anda dapat menyatukan bagian-bagian data yang terkait satu sama lain dan memberi nama pada setiap bagian untuk membuat kode Anda jelas. Dalam blok trait
dan impl
, Anda dapat mendefinisikan metode, yang merupakan fungsi terkait dengan tipe dan memungkinkan Anda menentukan perilaku yang dimiliki oleh instansi tipe Anda.
Namun, struct bukanlah satu-satunya cara Anda dapat membuat tipe kustom: mari beralih ke fitur enum dalam Cairo untuk menambahkan alat lain dalam kotak alat Anda.
Enum dan Pola Pencocokan
Pada bab ini, kita akan melihat enumerasi, juga dikenal sebagai enum.
Enum memungkinkan Anda untuk mendefinisikan tipe dengan cara mengenumerasi variant-nya yang mungkin. Pertama,
kita akan mendefinisikan dan menggunakan enum untuk menunjukkan bagaimana enum dapat mengkodekan makna bersama dengan
data. Selanjutnya, kita akan menjelajahi enum yang sangat berguna, yang disebut Option
, yang
mengungkapkan bahwa sebuah nilai dapat menjadi sesuatu atau tidak ada. Terakhir, kita akan melihat
bagaimana pola pencocokan dalam ekspresi match
membuatnya mudah untuk menjalankan kode yang berbeda
untuk nilai-nilai yang berbeda dari sebuah enum.
Enumerasi
Enum, singkatan dari "enumerations," adalah cara untuk mendefinisikan tipe data kustom yang terdiri dari satu set nilai bernama yang tetap, disebut variant. Enum berguna untuk mewakili kumpulan nilai terkait di mana setiap nilai bersifat berbeda dan memiliki makna tertentu.
Variant dan Nilai Enum
Berikut adalah contoh sederhana dari sebuah enum:
{{#include ../listings/ch06-enums-and-pattern-matching/no_listing_01_enum_example/src/lib.cairo:enum_example}}
Pada contoh ini, kami telah mendefinisikan sebuah enum yang disebut Direction
dengan empat variant: North
, East
, South
, dan West
. Konvensi penamaan yang digunakan adalah PascalCase untuk variant enum. Setiap variant mewakili nilai yang berbeda dari tipe Direction. Pada contoh ini, variant-variant tidak memiliki nilai terkait. Salah satu variant dapat diinstansiasi menggunakan sintaks ini:
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no_listing_01_enum_example/src/lib.cairo:here}}
Mudah untuk menulis kode yang berbeda tergantung pada variant dari sebuah instance enum, seperti pada contoh ini untuk menjalankan kode spesifik sesuai dengan sebuah Direction. Anda dapat mempelajari lebih lanjut tentang hal ini pada halaman Konstruksi Aliran Kontrol Match.
Enum Kombinasi dengan Tipe Kustom
Enum juga dapat digunakan untuk menyimpan data yang lebih menarik yang terkait dengan setiap variant. Sebagai contoh:
{{#include ../listings/ch06-enums-and-pattern-matching/no_listing_02_enum_message/src/lib.cairo:message}}
Pada contoh ini, enum Message
memiliki tiga variant: Quit
, Echo
, dan Move
, masing-masing dengan tipe yang berbeda:
Quit
tidak memiliki nilai terkait.Echo
adalah sebuah bilangan bulat tunggal.Move
adalah sebuah tuple dari dua nilai u128.
Anda bahkan dapat menggunakan Struct atau Enum lain yang Anda definisikan di dalam salah satu variant Enum Anda.
Implementasi Trait untuk Enum
Di Cairo, Anda dapat mendefinisikan trait dan mengimplementasikannya untuk enum kustom Anda. Ini memungkinkan Anda untuk mendefinisikan metode dan perilaku yang terkait dengan enum. Berikut contoh dari pendefinisian sebuah trait dan implementasinya untuk enum Message
sebelumnya:
{{#include ../listings/ch06-enums-and-pattern-matching/no_listing_02_enum_message/src/lib.cairo:trait_impl}}
Pada contoh ini, kami mengimplementasikan trait Processing
untuk Message
. Berikut adalah cara penggunaannya untuk memproses pesan Quit
:
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no_listing_02_enum_message/src/lib.cairo:main}}
Menjalankan kode ini akan mencetak quitting
.
Enum Option dan Keuntungannya
Enum Option adalah enum standar dalam Cairo yang mewakili konsep dari nilai opsional. Enum ini memiliki dua variant: Some: T
dan None: ()
. Some: T
menunjukkan bahwa ada sebuah nilai dengan tipe T
, sedangkan None
mewakili ketiadaan nilai.
enum Option<T> {
Some: T,
None: (),
}
Enum Option
berguna karena memungkinkan Anda untuk secara eksplisit mewakili kemungkinan dari sebuah nilai yang tidak ada, membuat kode Anda lebih ekspresif dan lebih mudah untuk dipahami. Menggunakan Option
juga dapat membantu mencegah bug yang disebabkan oleh penggunaan nilai null
yang tidak terinisialisasi atau tidak terduga.
Sebagai contoh, berikut adalah sebuah fungsi yang mengembalikan indeks dari elemen pertama dalam sebuah array dengan nilai tertentu, atau None jika elemen tersebut tidak ada.
Kami menunjukkan dua pendekatan untuk fungsi di atas:
- Pendekatan Rekursif
find_value_recursive
- Pendekatan Iteratif
find_value_iterative
Catatan: di masa depan akan lebih baik jika digantikan contoh yang lebih sederhana menggunakan perulangan dan tanpa kode terkait gas.
{{#include ../listings/ch06-enums-and-pattern-matching/no_listing_03_enum_option/src/lib.cairo}}
Menjalankan kode ini akan mencetak it worked
.
Konstruksi Aliran Kontrol Match
Cairo memiliki sebuah konstruksi aliran kontrol yang sangat kuat yang disebut match
yang memungkinkan Anda untuk membandingkan sebuah nilai terhadap serangkaian pola kemudian menjalankan kode berdasarkan pola mana yang cocok. Pola-pola dapat terdiri dari nilai literal, nama variabel, wildcard, dan banyak hal lainnya. Keunggulan dari match
datang dari ekspresivitas pola-pola dan fakta bahwa kompilator memastikan bahwa semua kasus yang mungkin telah ditangani.
Bayangkan ekspresi match seperti mesin penggolong koin: koin-koin meluncur di jalur dengan lubang-lubang berbagai ukuran di sepanjangnya, dan setiap koin jatuh melalui lubang pertama yang cocok. Dengan cara yang sama, nilai-nilai melewati setiap pola dalam match, dan pada pola pertama yang sesuai dengan nilai, nilai tersebut jatuh ke dalam blok kode yang terkait untuk digunakan selama eksekusi.
Berbicara tentang koin, mari gunakan mereka sebagai contoh menggunakan match! Kita dapat menulis sebuah fungsi yang mengambil koin AS yang tidak diketahui dan, dengan cara yang mirip dengan mesin penghitungan, menentukan jenis koin tersebut dan mengembalikan nilainya dalam sen, seperti yang ditunjukkan dalam Listing 6-3.
{{#include ../listings/ch06-enums-and-pattern-matching/listing_05_03/src/lib.cairo:all}}
Listing 6-3: Sebuah enum dan ekspresi match yang memiliki variant dari enum sebagai polanya
Mari kita bahas match
dalam fungsi value_in_cents
. Pertama, kita menyebutkan kata kunci match
diikuti oleh sebuah ekspresi, yang dalam hal ini adalah nilai coin
. Ini mirip sekali dengan ekspresi kondisional yang digunakan dengan if, tetapi ada perbedaan besar: dengan if, kondisi harus dievaluasi menjadi nilai Boolean, tetapi di sini bisa berupa tipe apa saja. Tipe dari coin
dalam contoh ini adalah enum Coin
yang kita definisikan pada baris pertama.
Selanjutnya adalah arm
dari match
. Sebuah arm
memiliki dua bagian: sebuah pola dan beberapa kode. Arm pertama di sini memiliki pola yang merupakan nilai Coin::Penny(_)
dan kemudian operator =>
yang memisahkan antara pola dan kode yang akan dijalankan. Kode dalam hal ini hanyalah nilai 1
. Setiap arm dipisahkan dari yang lain dengan tanda koma.
Ketika ekspresi match
dieksekusi, ia membandingkan nilai yang dihasilkan dengan pola dari setiap arm, secara berurutan. Jika sebuah pola cocok dengan nilai, kode yang terkait dengan pola tersebut dieksekusi. Jika pola tersebut tidak cocok dengan nilai, eksekusi akan melanjutkan ke arm berikutnya, seperti pada mesin penggolong koin. Kita dapat memiliki sebanyak yang kita butuhkan: dalam contoh di atas, match kita memiliki empat arm.
Di Cairo, urutan arm harus mengikuti urutan yang sama dengan enum.
Kode yang terkait dengan setiap arm adalah sebuah ekspresi, dan nilai hasil dari ekspresi pada arm yang cocok adalah nilai yang akan dikembalikan untuk seluruh ekspresi match.
Biasanya, kita tidak menggunakan kurung kurawal jika kode arm match singkat, seperti pada contoh kami di mana setiap arm hanya mengembalikan nilai. Jika Anda ingin menjalankan beberapa baris kode dalam arm match, Anda harus menggunakan kurung kurawal, dengan koma mengikuti setelah arm. Sebagai contoh, kode berikut mencetak "Lucky penny!" setiap kali metode dipanggil dengan Coin::Penny
, tetapi masih mengembalikan nilai terakhir dari blok, yaitu 1
:
{{#include ../listings/ch06-enums-and-pattern-matching/no_listing_04_match_arms/src/lib.cairo:here}}
Pola yang Terikat pada Nilai
Fitur lain yang berguna dari arm match adalah bahwa mereka dapat terikat pada bagian-bagian dari nilai yang cocok dengan pola. Inilah cara kita dapat mengekstrak nilai dari variant enum.
Sebagai contoh, mari ubah salah satu variant enum kita untuk menyimpan data di dalamnya. Dari tahun 1999 hingga 2008, Amerika Serikat memprangkai koin seperempat dengan desain yang berbeda untuk setiap dari 50 negara bagian di satu sisi. Koin lain tidak mendapatkan desain negara bagian, jadi hanya seperempat memiliki nilai tambahan ini. Kita dapat menambahkan informasi ini ke enum kita dengan mengubah variant Quarter
untuk menyertakan nilai UsState
yang disimpan di dalamnya, yang kita lakukan dalam Listing 6-4.
{{#include ../listings/ch06-enums-and-pattern-matching/listing_05_04/src/lib.cairo:enum_def}}
Listing 6-4: Sebuah enum Coin
di mana variant Quarter
juga menyimpan nilai UsState
Bayangkan seorang teman mencoba mengumpulkan semua 50 koin negara bagian. Ketika kita menyortir koin kepingan kepingan berdasarkan jenis koinnya, kita juga akan menyebutkan nama negara bagian yang terkait dengan setiap koin seperempat sehingga jika itu salah satu yang teman kita tidak miliki, mereka bisa menambahkannya ke koleksi mereka.
Pada ekspresi match untuk kode ini, kita menambahkan variabel bernama state
ke dalam pola yang cocok dengan nilai dari variant Coin::Quarter
. Ketika sebuah Coin::Quarter
cocok, variabel state
akan terikat pada nilai dari negara bagian dari koin tersebut. Kemudian kita dapat menggunakan state
dalam kode untuk arm tersebut, seperti ini:
{{#include ../listings/ch06-enums-and-pattern-matching/listing_05_04/src/lib.cairo:function}}
Untuk mencetak nilai dari sebuah variant dari enum di Cairo, kita perlu menambahkan implementasi untuk fungsi print
dari debug::PrintTrait
:
{{#include ../listings/ch06-enums-and-pattern-matching/listing_05_04/src/lib.cairo:print_impl}}
Jika kita memanggil value_in_cents(Coin::Quarter(UsState::Alaska))
, coin
akan menjadi Coin::Quarter(UsState::Alaska)
. Ketika kita membandingkan nilai tersebut dengan setiap arm match, tidak ada yang cocok sampai kita mencapai Coin::Quarter(state)
. Pada titik itu, pengikatan untuk state akan menjadi nilai UsState::Alaska
. Kita kemudian dapat menggunakan pengikatan itu dalam PrintTrait
, sehingga mendapatkan nilai dalam variant enum Quarter
.
Pencocokan dengan Opsi
Pada bagian sebelumnya, kita ingin mengambil nilai T
dalam kasus Some
saat menggunakan Option<T>
; kita juga dapat menangani Option<T>
menggunakan match
, seperti yang kita lakukan dengan enum Coin
! Alih-alih membandingkan koin, kita akan membandingkan variant dari Option<T>
, tetapi cara kerja ekspresi match
tetap sama.
Misalkan kita ingin menulis sebuah fungsi yang mengambil Option<u8>
dan, jika ada nilai di dalamnya, menambahkan 1
ke nilai tersebut. Jika tidak ada nilai di dalamnya, fungsi tersebut harus mengembalikan nilai None
dan tidak melakukan operasi apa pun.
Fungsi ini sangat mudah ditulis, berkat match
, dan akan terlihat seperti pada Listing 6-5.
{{#include ../listings/ch06-enums-and-pattern-matching/listing_05_05/src/lib.cairo:all}}
Listing 6-5: Sebuah fungsi yang menggunakan ekspresi match
pada Option<u8>
Perhatikan bahwa arm-arm Anda harus mengikuti urutan yang sama dengan enum yang didefinisikan di OptionTrait
dari lib Cairo inti.
enum Option<T> {
Some: T,
None,
}
Mari kita periksa eksekusi pertama dari plus_one
lebih detail. Ketika kita panggil plus_one(five)
, variabel x
di dalam tubuh plus_one
akan memiliki nilai Some(5)
. Kita kemudian membandingkannya dengan setiap arm match:
{{#include ../listings/ch06-enums-and-pattern-matching/listing_05_05/src/lib.cairo:option_some}}
Apakah nilai Option::Some(5)
cocok dengan pola Option::Some(val)
? Iya! Kita memiliki variant yang sama. val
mengikat pada nilai yang terdapat di dalam Option::Some
, jadi val
mengambil nilai 5
. Kode dalam arm match kemudian dieksekusi, sehingga kita menambahkan 1
ke nilai val
dan membuat nilai Option::Some
baru dengan total 6
di dalamnya. Karena arm pertama cocok, tidak ada arm lain yang dibandingkan.
Sekarang mari kita pertimbangkan panggilan kedua dari plus_one
dalam fungsi utama kita, di mana x
adalah Option::None
. Kita masuk ke dalam match dan membandingkannya dengan arm pertama:
{{#include ../listings/ch06-enums-and-pattern-matching/listing_05_05/src/lib.cairo:option_some}}
Nilai Option::Some(val)
tidak cocok dengan pola Option::None
, jadi kita melanjutkan ke arm berikutnya:
{{#include ../listings/ch06-enums-and-pattern-matching/listing_05_05/src/lib.cairo:option_none}}
Cocok! Tidak ada nilai untuk ditambahkan, sehingga program berhenti dan mengembalikan nilai Option::None
pada sisi kanan dari =>
.
Menggabungkan match
dan enum berguna dalam banyak situasi. Anda akan melihat pola ini banyak digunakan dalam kode Cairo: match
terhadap sebuah enum, mengikat sebuah variabel pada data di dalamnya, lalu menjalankan kode berdasarkan hal tersebut. Sedikit sulit pada awalnya, tetapi setelah Anda terbiasa, Anda akan berharap memiliki ini dalam semua bahasa. Ini secara konsisten menjadi favorit pengguna.
Pencocokan Adalah Exhaustive
Ada satu aspek lain dari match yang perlu kita diskusikan: pola-pola di dalam arm harus mencakup semua kemungkinan. Pertimbangkan versi fungsi plus_one
kita yang memiliki bug dan tidak akan dikompilasi:
{{#include ../listings/ch06-enums-and-pattern-matching/no_listing_07_missing_match_arm/src/lib.cairo:here}}
$ scarb cairo-run --available-gas=200000000
error: Unsupported match. Currently, matches require one arm per variant,
in the order of variant definition.
--> test.cairo:34:5
match x {
^*******^
Error: failed to compile: ./src/test.cairo
Cairo tahu bahwa kami tidak mencakup setiap kasus yang mungkin, dan bahkan tahu pola mana yang kami lupakan! Matches di Cairo adalah exhaustif: kami harus mencakup setiap kemungkinan terakhir agar kode menjadi valid. Terutama dalam kasus Option<T>
, ketika Cairo mencegah kita untuk lupa menangani kasus None
secara eksplisit, ia melindungi kita dari mengasumsikan bahwa kita memiliki nilai ketika kita mungkin memiliki null, sehingga membuat kesalahan miliaran dolar yang dibahas sebelumnya menjadi tidak mungkin.
Pencocokan 0 dan Operator _
Dengan menggunakan enum, kita juga dapat melakukan tindakan khusus untuk beberapa nilai tertentu, tetapi untuk semua nilai lainnya melakukan satu tindakan default. Saat ini hanya 0
dan operator _
yang didukung.
Bayangkan kita sedang mengimplementasikan sebuah permainan di mana Anda mendapatkan nomor acak antara 0 dan 7. Jika Anda mendapatkan 0, Anda menang. Untuk semua nilai lainnya, Anda kalah. Berikut adalah match
yang menerapkan logika tersebut, dengan nomor diinisialisasi secara langsung daripada menggunakan nilai acak.
{{#include ../listings/ch06-enums-and-pattern-matching/no_listing_06_match_zero/src/lib.cairo:here}}
Pada arm pertama, pola yang digunakan adalah nilai literal 0. Pada arm terakhir yang mencakup semua nilai lainnya, polanya adalah karakter _
. Kode ini dapat dikompilasi, meskipun kita tidak mencantumkan semua nilai yang mungkin dimiliki oleh felt252
, karena pola terakhir akan cocok dengan semua nilai yang tidak tercantum secara spesifik. Pola tangkap-semuanya ini memenuhi persyaratan bahwa match
harus eksaustif. Perlu diingat bahwa kita harus menempatkan arm tangkap-semuanya terakhir karena pola dievaluasi secara berurutan. Jika kita meletakkan arm tangkap-semuanya di awal, arm lainnya tidak akan pernah dijalankan, sehingga Cairo akan memberikan peringatan jika kita menambahkan arm setelah arm tangkap-semuanya!
Mengelola Proyek Cairo dengan Package, Crate, dan Modul
Ketika Anda menulis program-program besar, mengorganisir kode Anda akan menjadi semakin penting. Dengan mengelompokkan fungsionalitas terkait dan memisahkan kode dengan fitur-fitur yang berbeda, Anda akan menjelaskan di mana menemukan kode yang mengimplementasikan fitur tertentu dan ke mana harus pergi untuk mengubah cara kerja suatu fitur.
Program-program yang telah kita tulis sebelumnya berada dalam satu modul di satu file. Seiring dengan pertumbuhan proyek, Anda sebaiknya mengorganisir kode dengan membaginya ke dalam beberapa modul dan beberapa file. Seiring dengan pertumbuhan sebuah Package, Anda dapat mengekstrak bagian-bagian ke dalam crate terpisah yang menjadi dependensi eksternal. Bab ini mencakup semua teknik tersebut.
Kami juga akan membahas tentang mengenkapsulasi detail implementasi, yang memungkinkan Anda untuk menggunakan kembali kode pada level yang lebih tinggi: setelah Anda mengimplementasikan sebuah operasi, kode lain dapat memanggil kode Anda tanpa harus mengetahui bagaimana cara implementasinya bekerja.
Konsep terkait adalah cakupan (scope): konteks bertingkat di mana kode ditulis memiliki serangkaian nama yang didefinisikan sebagai "dalam cakupan." Saat membaca, menulis, dan mengompilasi kode, para pemrogram dan kompilator perlu tahu apakah suatu nama tertentu pada suatu tempat tertentu merujuk pada variabel, fungsi, struktur, enumerasi, modul, konstanta, atau item lainnya, dan apa arti dari item tersebut. Anda dapat membuat cakupan dan mengubah nama-nama yang berada dalam atau di luar cakupan. Anda tidak dapat memiliki dua item dengan nama yang sama dalam cakupan yang sama.
Cairo memiliki beberapa fitur yang memungkinkan Anda untuk mengelola organisasi kode Anda. Fitur-fitur ini, kadang-kadang secara kolektif disebut sebagai sistem modul, meliputi:
- Package: Fitur Scarb yang memungkinkan Anda membangun, menguji, dan berbagi crate.
- Crate: Sebuah pohon modul yang sesuai dengan satu unit kompilasi. Ini memiliki direktori root, dan modul root yang didefinisikan dalam file
lib.cairo
di bawah direktori ini. - Modul dan use: Memungkinkan Anda mengendalikan organisasi dan cakupan dari item-item.
- Path: Cara untuk menamai sebuah item, seperti struct, fungsi, atau modul.
Dalam bab ini, kami akan membahas semua fitur ini, mendiskusikan bagaimana mereka saling berinteraksi, dan menjelaskan cara menggunakan mereka untuk mengelola cakupan. Pada akhirnya, Anda seharusnya memiliki pemahaman yang kuat tentang sistem modul dan mampu bekerja dengan cakupan seperti seorang profesional!
Paket dan Crate
Apa itu crate?
Sebuah crate merupakan jumlah kode terkecil yang dipertimbangkan oleh kompiler Cairo pada suatu waktu. Bahkan jika Anda menjalankan cairo-compile
daripada scarb build
dan menyertakan sebuah file kode sumber tunggal, kompiler akan mempertimbangkan file tersebut sebagai sebuah crate. Crate dapat berisi modul-modul, dan modul-modul tersebut mungkin didefinisikan dalam file-file lain yang dikompilasi bersama dengan crate tersebut, seperti yang akan dibahas pada bagian-bagian selanjutnya.
Apa itu crate root?
Crate root merupakan file sumber lib.cairo
yang menjadi awal dari kompilasi oleh kompiler Cairo dan membentuk modul root dari crate Anda (kami akan menjelaskan modul secara mendalam pada bagian “Defining Modules to Control Scope”).
Apa itu sebuah paket?
Sebuah paket Cairo adalah kumpulan satu atau lebih crate dengan file Scarb.toml yang menjelaskan bagaimana cara membangun crate-crate tersebut. Ini memungkinkan pemisahan kode menjadi bagian-bagian yang lebih kecil dan dapat digunakan kembali, serta memfasilitasi manajemen dependensi yang lebih terstruktur.
Membuat Paket dengan Scarb
Anda dapat membuat paket Cairo baru menggunakan perangkat baris perintah scarb. Untuk membuat paket baru, jalankan perintah berikut:
scarb new my_package
Perintah ini akan membuat direktori paket baru bernama my_package
dengan struktur berikut:
my_package/
├── Scarb.toml
└── src
└── lib.cairo
src/
adalah direktori utama di mana semua file sumber Cairo untuk paket akan disimpan.lib.cairo
adalah modul root default dari crate, yang juga merupakan titik masuk utama dari paket.Scarb.toml
adalah file manifest paket, yang berisi metadata dan opsi konfigurasi untuk paket, seperti dependensi, nama paket, versi, dan penulis. Anda dapat menemukan dokumentasi tentang ini pada referensi scarb.
[package]
name = "my_package"
version = "0.1.0"
[dependencies]
# foo = { path = "vendor/foo" }
Saat Anda mengembangkan paket Anda, Anda mungkin ingin mengorganisir kode Anda ke dalam beberapa file sumber Cairo. Anda dapat melakukannya dengan membuat file-file .cairo
tambahan di dalam direktori src
atau subdirektorinya.
Mendefinisikan Modul untuk Mengendalikan Cakupan
Pada bagian ini, kita akan membicarakan tentang modul dan bagian-bagian lain dari sistem modul, yaitu paths yang memungkinkan Anda untuk memberi nama pada item-item dan kata kunci use
yang membawa sebuah path ke dalam cakupan.
Pertama, kita akan memulai dengan daftar aturan sebagai referensi mudah saat Anda mengorganisir kode Anda di masa mendatang. Kemudian, kita akan menjelaskan setiap aturan tersebut secara rinci.
Cheet Sheet Modul
Di sini, kami memberikan referensi cepat tentang bagaimana modul, paths, dan kata kunci use
bekerja dalam kompiler, serta bagaimana sebagian besar pengembang mengorganisir kode mereka. Kami akan melalui contoh-contoh dari setiap aturan ini sepanjang bab ini, namun ini adalah tempat yang bagus untuk merujuk sebagai pengingat tentang cara kerja modul. Anda dapat membuat proyek Scarb baru dengan scarb new backyard
untuk mengikuti penjelasan ini.
-
Mulai dari crate root: Saat mengompilasi sebuah crate, kompiler pertama-tama melihat di file crate root (src/lib.cairo) untuk kode yang akan dikompilasi.
-
Deklarasi modul: Di file crate root, Anda dapat mendeklarasikan modul-modul baru; misalnya, Anda mendeklarasikan modul "garden" dengan
mod garden;
. Kompiler akan mencari kode modul tersebut di tempat-tempat berikut:-
Secara inline, dalam kurung kurawal yang menggantikan titik koma setelah
mod garden;
.// file crate root (src/lib.cairo) mod garden { // kode yang mendefinisikan modul garden ada di sini }
-
Di dalam file src/garden.cairo
-
-
Deklarasi submodul: Di file selain crate root, Anda dapat mendeklarasikan submodul. Sebagai contoh, Anda mungkin mendeklarasikan
mod vegetables;
di src/garden.cairo. Kompiler akan mencari kode submodul tersebut di dalam direktori yang dinamai sesuai dengan modul induk pada tempat-tempat berikut:-
Secara inline, langsung mengikuti
mod vegetables
, di dalam kurung kurawal sebagai pengganti titik koma.// file src/garden.cairo mod vegetables { // kode yang mendefinisikan submodul vegetables ada di sini }
-
Di dalam file src/garden/vegetables.cairo
-
-
Paths menuju kode dalam modul: Setelah sebuah modul menjadi bagian dari crate Anda, Anda dapat merujuk pada kode dalam modul tersebut dari mana pun di dalam crate yang sama, menggunakan path ke kode tersebut. Sebagai contoh, sebuah tipe
Asparagus
dalam modul sayuran taman akan ditemukan padabackyard::garden::vegetables::Asparagus
. -
Kata kunci
use
: Dalam sebuah cakupan, kata kunciuse
menciptakan pintasan ke item untuk mengurangi pengulangan dari path yang panjang. Di dalam cakupan yang dapat merujuk kebackyard::garden::vegetables::Asparagus
, Anda dapat membuat pintasan denganuse backyard::garden::vegetables::Asparagus;
dan setelah itu Anda hanya perlu menulisAsparagus
untuk menggunakan tipe tersebut dalam cakupan tersebut.
Berikut ini kami membuat sebuah kardus yang diberi nama backyard
yang menggambarkan aturan-aturan ini. Direktori kardus, juga bernama backyard
, berisi file dan direktori-direktori berikut:
backyard/
├── Scarb.toml
└── src
├── garden
│ └── vegetables.cairo
├── garden.cairo
└── lib.cairo
File root kardus dalam hal ini adalah src/lib.cairo, dan berisi:
Nama File: src/lib.cairo
{{#include ../listings/ch07-managing-cairo-projects-with-packages-crates-and-modules/no_listing_01_lib/src/lib.cairo}}
Baris mod garden;
memberi tahu kompiler untuk menyertakan kode yang ditemukan di src/garden.cairo, yang berisi:
Nama File: src/garden.cairo
mod vegetables;
Di sini, mod vegetables;
berarti kode di src/garden/vegetables.cairo juga disertakan. Kode itu adalah:
{{#include ../listings/ch07-managing-cairo-projects-with-packages-crates-and-modules/no_listing_02_garden/src/lib.cairo}}
Baris use garden::vegetables::Asparagus;
memungkinkan kita membawa tipe Asparagus
ke dalam lingkup, sehingga kita dapat menggunakannya di dalam fungsi main
.
Sekarang mari kita masuk ke dalam rincian aturan-aturan ini dan tunjukkan mereka dalam tindakan!
Mengelompokkan Kode yang Berkaitan dalam Modul
Modul memungkinkan kita mengorganisir kode dalam sebuah kardus untuk keterbacaan dan penggunaan ulang yang mudah. Sebagai contoh, mari kita tulis kardus pustaka yang menyediakan fungsionalitas sebuah restoran. Kita akan menentukan tanda tangan fungsi tetapi meninggalkan tubuhnya kosong untuk fokus pada organisasi kode, bukan implementasi restoran.
Dalam industri restoran, beberapa bagian dari restoran disebut sebagai front of house dan yang lain sebagai back of house. Front of house adalah tempat pelanggan berada; ini mencakup tempat tuan rumah duduk, pelayan mengambil pesanan dan pembayaran, dan barman membuat minuman. Back of house adalah tempat koki dan karyawan dapur bekerja, pencuci piring membersihkan, dan manajer melakukan pekerjaan administratif.
Untuk mengorganisir kardus kita dengan cara ini, kita dapat mengatur fungsinya ke dalam modul-modul bertingkat. Buat paket baru dengan nama restaurant
dengan menjalankan scarb new restaurant
; kemudian masukkan kode pada Listing 7-1 ke dalam src/lib.cairo untuk mendefinisikan beberapa modul dan tanda tangan fungsi. Berikut adalah bagian front of house:
Nama File: src/lib.cairo
{{#include ../listings/ch07-managing-cairo-projects-with-packages-crates-and-modules/listing_06_01/src/lib.cairo}}
Listing 7-1: Modul front_of_house
yang berisi modul-modul lain yang kemudian berisi fungsi-fungsi
Kita mendefinisikan modul dengan kata kunci mod
diikuti oleh nama modul
(dalam hal ini, front_of_house
). Tubuh modul kemudian ditempatkan di dalam kurung kurawal. Di dalam modul, kita dapat menempatkan modul-modul lain, seperti dalam hal ini dengan modul-modul hosting
dan serving
. Modul juga dapat berisi definisi untuk item lain, seperti struktur, enumerasi, konstan, trait, dan—seperti pada Listing
6-1—fungsi.
Dengan menggunakan modul, kita dapat mengelompokkan definisi yang berkaitan bersama dan memberi nama mengapa mereka berkaitan. Programer yang menggunakan kode ini dapat menavigasi kode berdasarkan kelompok daripada harus membaca semua definisi, membuatnya lebih mudah untuk menemukan definisi yang relevan bagi mereka. Programer yang menambahkan fungsionalitas baru ke dalam kode ini akan tahu di mana menempatkan kode untuk menjaga agar program terorganisir.
Sebelumnya, kami menyebutkan bahwa src/lib.cairo disebut sebagai root kardus. Nama ini diberikan karena isi file ini membentuk modul yang dinamai sesuai nama kardus di root struktur modul kardus tersebut, dikenal sebagai pohon modul.
Listing 7-2 menunjukkan pohon modul untuk struktur pada Listing 7-1.
restaurant
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
Listing 7-2: Pohon modul untuk kode pada Listing 6-1
Pohon ini menunjukkan bagaimana beberapa modul bersarang di dalam yang lain; misalnya,
hosting
bersarang di dalam front_of_house
. Pohon ini juga menunjukkan bahwa beberapa modul
adalah sibling satu sama lain, yang berarti mereka didefinisikan di dalam modul yang sama;
hosting
dan serving
adalah sibling yang didefinisikan di dalam front_of_house
. Jika modul
A berada di dalam modul B, kita katakan bahwa modul A adalah child dari modul B
dan bahwa modul B adalah parent dari modul A. Perhatikan bahwa seluruh pohon modul berakar di bawah nama eksplisit kardus restaurant
.
Pohon modul mungkin mengingatkan Anda pada pohon direktori sistem file di komputer Anda; ini adalah perbandingan yang sangat tepat! Sama seperti direktori dalam sistem file, Anda menggunakan modul untuk mengorganisir kode Anda. Dan sama seperti file dalam sebuah direktori, kita membutuhkan cara untuk menemukan modul-modul kita.
Path untuk Merujuk ke Item dalam Pohon Modul
Untuk menunjukkan kepada Cairo di mana menemukan suatu item dalam pohon modul, kita menggunakan path dengan cara yang sama seperti kita menggunakan path saat menjelajahi sistem file. Untuk memanggil sebuah fungsi, kita perlu tahu path-nya.
Sebuah path dapat memiliki dua bentuk:
-
Sebuah absolute path adalah path lengkap yang dimulai dari root kardus. Absolute path dimulai dengan nama kardus.
-
Sebuah relative path dimulai dari modul saat ini.
Keduanya, absolute dan relative paths diikuti oleh satu atau lebih identifikasi yang dipisahkan oleh titik dua ganda (
::
).
Untuk mengilustrasikan konsep ini, mari kita ambil contoh Listing 7-1 untuk restoran yang kita gunakan dalam bab terakhir. Kita memiliki sebuah kardus bernama restaurant
di dalamnya terdapat modul bernama front_of_house
yang berisi modul bernama hosting
. Modul hosting
berisi fungsi bernama add_to_waitlist
. Kita ingin memanggil fungsi add_to_waitlist
dari fungsi eat_at_restaurant
. Kita perlu memberi tahu Cairo path ke fungsi add_to_waitlist
agar bisa menemukannya.
Nama File: src/lib.cairo
{{#include ../listings/ch07-managing-cairo-projects-with-packages-crates-and-modules/listing_06_03/src/lib.cairo}}
Listing 7-3: Memanggil fungsi add_to_waitlist
menggunakan absolute dan relative paths
Pertama kali kita memanggil fungsi add_to_waitlist
di dalam eat_at_restaurant
,
kita menggunakan absolute path. Fungsi add_to_waitlist
didefinisikan dalam kardus yang sama dengan eat_at_restaurant
. Dalam Cairo, absolute paths dimulai dari root kardus, yang perlu Anda sebutkan dengan menggunakan nama kardus.
Kedua kalinya kita memanggil add_to_waitlist
, kita menggunakan relative path. Path dimulai dengan front_of_house
, nama modul
yang didefinisikan pada tingkat yang sama dalam pohon modul seperti eat_at_restaurant
. Di sini, ekuivalen sistem file akan menggunakan path
./front_of_house/hosting/add_to_waitlist
. Memulai dengan nama modul berarti
bahwa path ini bersifat relatif terhadap modul saat ini.
Memulai Relative Paths dengan super
Memilih apakah akan menggunakan super
atau tidak adalah keputusan yang akan Anda ambil
berdasarkan proyek Anda, dan tergantung pada apakah Anda lebih mungkin memindahkan definisi item
terpisah dari atau bersamaan dengan kode yang menggunakan item tersebut.
Nama File: src/lib.cairo
{{#include ../listings/ch07-managing-cairo-projects-with-packages-crates-and-modules/listing_06_04/src/lib.cairo}}
Listing 7-4: Memanggil fungsi menggunakan relative path yang dimulai dengan super
Di sini Anda dapat melihat secara langsung bahwa Anda dapat mengakses modul induk dengan mudah menggunakan super
, yang tidak mungkin sebelumnya.
Membawa Jalur ke dalam Lingkup dengan Kata Kunci use
Menulis jalur untuk memanggil fungsi dapat terasa merepotkan dan repetitif. Untungnya, ada cara untuk menyederhanakan proses ini: kita dapat membuat pintasan ke suatu jalur dengan kata kunci use
sekali, dan kemudian menggunakan nama yang lebih pendek di mana pun di dalam lingkup.
Pada Listing 7-5, kita membawa modul restaurant::front_of_house::hosting
ke dalam lingkup fungsi eat_at_restaurant
sehingga kita hanya perlu menyebutkan hosting::add_to_waitlist
untuk memanggil fungsi add_to_waitlist
di dalam eat_at_restaurant
.
Nama File: src/lib.cairo
{{#include ../listings/ch07-managing-cairo-projects-with-packages-crates-and-modules/listing_06_05/src/lib.cairo}}
Listing 7-5: Membawa modul ke dalam lingkup dengan
use
Menambahkan use
dan suatu jalur dalam suatu lingkup mirip dengan membuat symbolic link dalam sistem file. Dengan menambahkan use restaurant::front_of_house::hosting
di akar kerangka, hosting
sekarang adalah nama yang valid di dalam lingkup tersebut, seolah-olah modul hosting
telah didefinisikan di akar kerangka.
Perhatikan bahwa use
hanya membuat pintasan untuk lingkup tertentu di mana use
tersebut terjadi. Listing 7-6 memindahkan fungsi eat_at_restaurant
ke dalam modul anak baru yang diberi nama customer
, yang kemudian menjadi lingkup yang berbeda dari pernyataan use
, sehingga tubuh fungsi tidak akan dikompilasi:
Nama File: src/lib.cairo
{{#include ../listings/ch07-managing-cairo-projects-with-packages-crates-and-modules/listing_06_06/src/lib.cairo}}
Listing 7-6: Pernyataan use
hanya berlaku dalam lingkup
yang ada di dalamnya
Error kompilator menunjukkan bahwa pintasan tidak lagi berlaku di dalam modul customer
:
❯ scarb build
error: Identifier not found.
--> lib.cairo:11:9
hosting::add_to_waitlist();
^*****^
Membuat Jalur use
yang Idiomatik
Pada Listing 7-5, Anda mungkin bertanya-tanya mengapa kita menentukan use restaurant::front_of_house::hosting
dan kemudian memanggil hosting::add_to_waitlist
di
eat_at_restaurant
daripada menentukan jalur use
sampai ke
fungsi add_to_waitlist
untuk mencapai hasil yang sama, seperti pada Listing 7-7.
Nama File: src/lib.cairo
{{#include ../listings/ch07-managing-cairo-projects-with-packages-crates-and-modules/listing_06_07/src/lib.cairo}}
Listing 7-7: Membawa fungsi add_to_waitlist
ke dalam lingkup dengan use
, yang tidak idiomatik
Meskipun keduanya, Listing 7-5 dan 6-7, mencapai tugas yang sama, Listing 7-5 adalah cara yang idiomatik untuk membawa fungsi ke dalam lingkup dengan use
. Membawa modul induk fungsi ke dalam lingkup dengan use
berarti kita harus menyebutkan modul induk saat memanggil fungsi. Menyebutkan modul induk saat memanggil fungsi membuat jelas bahwa fungsi tersebut tidak didefinisikan secara lokal sambil tetap meminimalkan pengulangan dari jalur lengkap. Kode pada Listing 7-7 tidak jelas di mana add_to_waitlist
didefinisikan.
Di sisi lain, ketika membawa dalam struktur, enumerasi, trait, dan item lain dengan menggunakan use
, cara idiomatik adalah dengan menentukan jalur lengkap. Listing 7-8 menunjukkan cara idiomatik membawa trait ArrayTrait
dari pustaka inti ke dalam lingkup.
{{#include ../listings/ch07-managing-cairo-projects-with-packages-crates-and-modules/listing_06_08/src/lib.cairo}}
Listing 7-8: Membawa ArrayTrait
ke dalam lingkup dengan cara
idiomatik
Tidak ada alasan kuat di balik idiom ini: ini hanya konvensi yang muncul dalam komunitas Rust, dan orang-orang telah terbiasa membaca dan menulis kode Rust dengan cara ini. Karena Cairo memiliki banyak idiom yang sama dengan Rust, kami mengikuti konvensi ini juga.
Pengecualian dari idiom ini adalah jika kita membawa dua item dengan nama yang sama ke dalam lingkup yang sama dengan pernyataan use
, karena Cairo tidak mengizinkan hal tersebut.
Memberikan Nama Baru dengan Kata Kunci as
Ada solusi lain untuk masalah membawa dua jenis dengan nama yang sama ke dalam lingkup yang sama dengan use
: setelah jalur, kita dapat menentukan as
dan nama lokal baru, atau alias, untuk jenis tersebut. Listing 7-9 menunjukkan cara Anda dapat mengganti nama impor dengan as
:
Nama File: src/lib.cairo
{{#include ../listings/ch07-managing-cairo-projects-with-packages-crates-and-modules/listing_06_09/src/lib.cairo}}
Listing 7-9: Mengganti nama trait ketika dibawa ke dalam
lingkup dengan kata kunci as
Di sini, kami membawa ArrayTrait
ke dalam lingkup dengan alias Arr
. Sekarang kita dapat mengakses metode-metode trait dengan pengenal Arr
.
Mengimpor beberapa item dari modul yang sama
Ketika Anda ingin mengimpor beberapa item (seperti fungsi, struktur, atau enumerasi) dari modul yang sama di Cairo, Anda dapat menggunakan kurung kurawal {}
untuk mencantumkan semua item yang ingin Anda impor. Ini membantu menjaga kode Anda bersih dan mudah dibaca dengan menghindari daftar panjang pernyataan use
yang terpisah.
Syntax umum untuk mengimpor beberapa item dari modul yang sama adalah:
use module::{item1, item2, item3};
Berikut adalah contoh di mana kita mengimpor tiga struktur dari modul yang sama:
{{#include ../listings/ch07-managing-cairo-projects-with-packages-crates-and-modules/listing_06_10/src/lib.cairo}}
Listing 7-10: Mengimpor beberapa item dari modul yang sama
Mengulang Nama dalam Berkas Modul
Ketika kita membawa suatu nama ke dalam lingkup dengan kata kunci use
, nama yang tersedia dalam lingkup baru dapat diimpor seolah-olah telah didefinisikan dalam lingkup kode tersebut. Teknik ini disebut re-exporting karena kita membawa suatu item ke dalam lingkup, tetapi juga membuat item tersebut tersedia bagi orang lain untuk membawanya ke dalam lingkup mereka.
Sebagai contoh, mari re-export fungsi add_to_waitlist
pada contoh restoran:
Nama File: src/lib.cairo
{{#include ../listings/ch07-managing-cairo-projects-with-packages-crates-and-modules/listing_06_11/src/lib.cairo}}
Listing 7-11: Membuat suatu nama tersedia untuk kode apa pun digunakan
dari lingkup baru dengan pub use
Sebelum perubahan ini, kode eksternal harus memanggil fungsi add_to_waitlist
dengan menggunakan jalur
restaurant::front_of_house::hosting::add_to_waitlist()
. Sekarang bahwa use
ini telah mere-export modul hosting
dari modul akar, kode eksternal
sekarang dapat menggunakan jalur restaurant::hosting::add_to_waitlist()
sebagai gantinya.
Mere-export berguna ketika struktur internal kode Anda berbeda
dengan cara pemrogram yang memanggil kode Anda akan memikirkan domain tersebut. Sebagai
contoh, dalam metafora restoran ini, orang-orang yang menjalankan restoran berpikir
tentang "front of house" dan "back of house." Tetapi pelanggan yang mengunjungi restoran
mungkin tidak akan memikirkan bagian-bagian restoran dengan istilah tersebut. Dengan
use
, kita dapat menulis kode kita dengan satu struktur tetapi mengekspos struktur
yang berbeda. Melakukan hal tersebut membuat perpustakaan kita terorganisir dengan baik untuk pemrogram yang bekerja pada
perpustakaan dan pemrogram yang memanggil perpustakaan tersebut.
Menggunakan Paket Eksternal di Cairo dengan Scarb
Anda mungkin perlu menggunakan paket eksternal untuk memanfaatkan fungsionalitas yang disediakan oleh komunitas. Untuk menggunakan paket eksternal dalam proyek Anda dengan Scarb, ikuti langkah-langkah berikut:
Sistem dependensi masih dalam tahap pengembangan. Anda dapat memeriksa dokumentasi resmi.
Memisahkan Modul ke dalam Berkas yang Berbeda
Sejauh ini, semua contoh dalam bab ini mendefinisikan beberapa modul dalam satu file. Ketika modul menjadi besar, Anda mungkin ingin memindahkan definisinya ke file terpisah untuk membuat kode lebih mudah dinavigasi.
Sebagai contoh, mari mulai dari kode pada Listing 7-11 yang memiliki beberapa modul restoran. Kita akan mengekstrak modul ke dalam file daripada mendefinisikan semua modul di dalam file akar kerangka. Dalam hal ini, file akar kerangka adalah src/lib.cairo.
Pertama, kita akan mengekstrak modul front_of_house
ke dalam file tersendiri. Hapus kode di dalam kurung kurawal untuk modul front_of_house
, hanya meninggalkan deklarasi mod front_of_house;
, sehingga src/lib.cairo berisi kode seperti yang terlihat pada Listing 7-12. Perlu diingat bahwa ini tidak akan dikompilasi hingga kita membuat file src/front_of_house.cairo seperti yang terlihat pada Listing 7-13.
Nama File: src/lib.cairo
{{#include ../listings/ch07-managing-cairo-projects-with-packages-crates-and-modules/listing_06_12/src/lib.cairo}}
Listing 7-12: Mendeklarasikan modul front_of_house
yang
tubuhnya akan berada di src/front_of_house.cairo
Selanjutnya, letakkan kode yang berada di dalam kurung kurawal ke dalam file baru bernama src/front_of_house.cairo, seperti yang ditunjukkan pada Listing 7-13. Kompiler tahu untuk mencari di file ini karena menemui deklarasi modul di akar kerangka dengan nama front_of_house
.
Nama File: src/front_of_house.cairo
{{#include ../listings/ch07-managing-cairo-projects-with-packages-crates-and-modules/listing_06_13/src/lib.cairo}}
Listing 7-13: Definisi di dalam modul front_of_house
di src/front_of_house.cairo
Perhatikan bahwa Anda hanya perlu memuat suatu file menggunakan deklarasi mod
sekali dalam pohon modul Anda. Setelah kompiler tahu bahwa file itu bagian dari proyek (dan tahu di mana kode itu berada dalam pohon modul karena di mana Anda menempatkan pernyataan mod
), file lain dalam proyek Anda harus merujuk ke kode file yang dimuat menggunakan jalur ke tempat itu dideklarasikan, seperti yang dijelaskan dalam “Paths for Referring
to an Item in the Module Tree”. Dengan kata lain, mod
bukan operasi "include" yang mungkin telah Anda lihat dalam bahasa pemrograman lain.
Selanjutnya, kita akan mengekstrak modul hosting
ke dalam file terpisah. Proses ini sedikit berbeda karena hosting
adalah modul anak dari front_of_house
, bukan dari modul akar. Kita akan meletakkan file untuk hosting
dalam direktori baru yang akan dinamai sesuai dengan leluhurnya dalam pohon modul, dalam hal ini src/front_of_house/.
Untuk memulai memindahkan hosting
, kita ubah src/front_of_house.cairo untuk hanya berisi deklarasi modul hosting
:
Nama File: src/front_of_house.cairo
mod hosting;
Kemudian, kita membuat direktori src/front_of_house dan sebuah file hosting.cairo untuk berisi definisi yang dibuat di dalam modul hosting
:
Nama File: src/front_of_house/hosting.cairo
fn add_to_waitlist() {}
Jika kita malah meletakkan hosting.cairo di dalam direktori src, kompiler akan mengharapkan kode hosting.cairo berada dalam modul hosting
yang dideklarasikan di akar kerangka, dan tidak dideklarasikan sebagai anak dari modul front_of_house
. Aturan kompiler untuk file mana yang akan diperiksa untuk kode modul mana berarti direktori dan file lebih mendekati pohon modul.
Kita telah memindahkan kode setiap modul ke dalam file terpisah, dan pohon modul tetap sama. Panggilan fungsi di dalam eat_at_restaurant
akan berfungsi tanpa modifikasi apa pun, meskipun definisinya berada di file yang berbeda. Teknik ini memungkinkan Anda memindahkan modul ke file baru seiring pertumbuhannya.
Perlu diingat bahwa pernyataan use restaurant::front_of_house::hosting
di
src/lib.cairo juga tidak berubah, dan use
tidak memiliki dampak pada file mana
yang dikompilasi sebagai bagian dari crate. Kata kunci mod
mendeklarasikan modul, dan Cairo mencari di file dengan nama yang sama dengan modul untuk kode yang masuk ke dalam modul tersebut.
Ringkasan
Cairo memungkinkan Anda membagi paket menjadi beberapa crate dan crate menjadi modul
sehingga Anda dapat merujuk pada item yang didefinisikan di satu modul dari modul lainnya. Anda dapat
melakukan ini dengan menentukan jalur absolut atau relatif. Jalur-jalur ini dapat dibawa
ke dalam lingkup dengan pernyataan use
sehingga Anda dapat menggunakan jalur yang lebih pendek untuk penggunaan
item tersebut dalam lingkup tersebut. Kode modul secara default bersifat publik.
Tipe dan Trait Generik
Setiap bahasa pemrograman memiliki alat untuk mengatasi duplikasi konsep dengan efektif. Di Cairo, salah satu alat tersebut adalah generik: representasi abstrak untuk tipe konkret atau properti lain. Kita dapat menyatakan perilaku generik atau bagaimana mereka berhubungan dengan generik lain tanpa mengetahui apa yang akan menggantikan mereka saat mengompilasi dan menjalankan kode.
Fungsi, struktur, enumerasi, dan trait dapat menggunakan tipe generik sebagai bagian dari definisi mereka daripada tipe konkret seperti u32
atau ContractAddress
.
Generik memungkinkan kita untuk mengganti tipe tertentu dengan placeholder yang mewakili beberapa tipe untuk menghilangkan duplikasi kode.
Untuk setiap tipe konkret yang menggantikan tipe generik, kompiler membuat definisi baru, mengurangi waktu pengembangan bagi pemrogram, tetapi duplikasi kode pada tingkat kompilasi masih ada. Hal ini mungkin penting jika Anda menulis kontrak Starknet dan menggunakan generik untuk beberapa tipe yang akan menyebabkan peningkatan ukuran kontrak.
Selanjutnya, Anda akan belajar cara menggunakan trait untuk mendefinisikan perilaku secara generik. Anda dapat menggabungkan trait dengan tipe generik untuk membatasi tipe generik agar hanya menerima tipe-tipe yang memiliki perilaku tertentu, daripada hanya tipe apa pun.
Jenis Data Generik
Kami menggunakan generik untuk membuat definisi deklarasi item, seperti structs dan functions, yang kemudian dapat kita gunakan dengan banyak jenis data konkret yang berbeda. Di Cairo, kita dapat menggunakan generik saat mendefinisikan functions, structs, enums, traits, implementasi, dan metode! Pada bab ini, kita akan melihat bagaimana cara menggunakan tipe generik secara efektif dengan semua hal tersebut.
Fungsi Generik
Saat mendefinisikan fungsi yang menggunakan generik, kita menempatkan generik pada tanda tangan fungsi, di mana kita biasanya menentukan jenis data dari parameter dan nilai kembali. Sebagai contoh, bayangkan kita ingin membuat sebuah fungsi yang, dengan dua Array
item, akan mengembalikan yang terbesar. Jika kita perlu melakukan operasi ini untuk daftar jenis yang berbeda, maka kita harus mendefinisikan kembali fungsi tersebut setiap kali. Untungnya, kita dapat mengimplementasikan fungsi ini sekali menggunakan generik dan melanjutkan ke tugas lain.
{{#include ../listings/ch08-generic-types-and-traits/no_listing_01_missing_tdrop/src/lib.cairo}}
Fungsi largest_list
membandingkan dua daftar dengan tipe yang sama dan mengembalikan yang memiliki lebih banyak elemen serta menghapus yang lain. Jika Anda mengompilasi kode sebelumnya, Anda akan melihat bahwa itu akan gagal dengan kesalahan yang mengatakan bahwa tidak ada traits yang ditentukan untuk menghapus array dari jenis generik. Hal ini terjadi karena kompiler tidak memiliki cara untuk menjamin bahwa Array<T>
dapat dihapus saat menjalankan fungsi main
. Untuk menghapus array dari T
, kompiler harus tahu cara menghapus T
. Ini dapat diperbaiki dengan menentukan dalam tanda tangan fungsi largest_list
bahwa T
harus mengimplementasikan trait drop. Definisi fungsi largest_list
yang benar adalah sebagai berikut:
{{#rustdoc_include ../listings/ch08-generic-types-and-traits/no_listing_02_with_tdrop/src/lib.cairo}}
Fungsi largest_list
baru mencakup dalam definisinya persyaratan bahwa jenis generik apa pun yang ditempatkan di sana, itu harus dapat dihapus. Fungsi main
tetap tidak berubah, kompiler cukup cerdas untuk menyimpulkan jenis konkret apa yang digunakan dan apakah itu mengimplementasikan trait Drop
.
Kendala untuk Jenis Generik
Saat mendefinisikan jenis generik, berguna untuk memiliki informasi tentang mereka. Mengetahui trait mana yang diimplementasikan oleh jenis generik memungkinkan kita menggunakan mereka secara lebih efektif dalam logika fungsi dengan biaya membatasi jenis generik yang dapat digunakan dengan fungsi tersebut. Kita melihat contoh ini sebelumnya dengan menambahkan implementasi TDrop
sebagai bagian dari argumen generik dari largest_list
. Sementara TDrop
ditambahkan untuk memenuhi persyaratan kompiler, kita juga dapat menambahkan kendala untuk memperkaya logika fungsi kita.
Bayangkan kita ingin, diberikan sebuah daftar elemen dari suatu jenis generik T
, mencari elemen terkecil di antara mereka. Awalnya, kita tahu bahwa untuk elemen jenis T
dapat dibandingkan, itu harus mengimplementasikan trait PartialOrd
. Fungsi yang dihasilkan akan menjadi:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_03_missing_tcopy/src/lib.cairo}}
Fungsi smallest_element
menggunakan jenis generik T
yang mengimplementasikan trait PartialOrd
, mengambil snapshot dari Array<T>
sebagai parameter, dan mengembalikan salinan elemen terkecil. Karena parameter berjenis @Array<T>
, kita tidak perlu lagi menghapusnya pada akhir eksekusi dan oleh karena itu tidak memerlukan implementasi trait Drop
untuk T
juga. Mengapa ini tidak dikompilasi?
Saat mengindeks pada list
, nilai menghasilkan snap dari elemen yang diindeks, kecuali jika PartialOrd
diimplementasikan untuk @T
, kita perlu melepaskan elemen tersebut menggunakan *
. Operasi *
memerlukan salinan dari @T
ke T
, yang berarti bahwa T
harus mengimplementasikan trait Copy
. Setelah menyalin elemen bertipe @T
menjadi T
, sekarang ada variabel dengan tipe T
yang perlu dihapus, memerlukan T
untuk mengimplementasikan trait Drop
juga. Kita harus menambahkan implementasi trait Drop
dan Copy
untuk fungsi menjadi benar. Setelah memperbarui fungsi smallest_element
, kode yang dihasilkan akan menjadi:
{{#rustdoc_include ../listings/ch08-generic-types-and-traits/no_listing_04_with_tcopy/src/lib.cairo}}
Parameter Implementasi Generik Anonim (+
operator)
Sampai sekarang, kita selalu menentukan nama untuk setiap implementasi trait generik yang diperlukan: TPartialOrd
untuk PartialOrd<T>
, TDrop
untuk Drop<T>
, dan TCopy
untuk Copy<T>
.
Namun, sebagian besar waktu, kita tidak menggunakan implementasi dalam tubuh fungsi; kita hanya menggunakannya sebagai batasan. Dalam kasus-kasus ini, kita dapat menggunakan operator +
untuk menentukan bahwa jenis generik harus mengimplementasikan suatu trait tanpa memberi nama implementasinya. Ini disebut sebagai parameter implementasi generik anonim.
Sebagai contoh, +PartialOrd<T>
setara dengan impl TPartialOrd: PartialOrd<T>
.
Kita dapat menulis ulang tanda tangan fungsi smallest_element
sebagai berikut:
{{#rustdoc_include ../listings/ch08-generic-types-and-traits/no_listing_05_with_anonymous_impl/src/lib.cairo:1}}
Structs
Kita juga dapat mendefinisikan structs untuk menggunakan parameter jenis generik untuk satu atau lebih field menggunakan sintaks <>
, mirip dengan definisi fungsi. Pertama, kita mendeklarasikan nama parameter jenis di dalam tanda kurung sudut setelah nama struct. Kemudian kita menggunakan jenis generik dalam definisi struct di mana kita seharusnya menentukan jenis data konkret. Contoh kode berikut menunjukkan definisi Wallet<T>
yang memiliki field balance
bertipe T
.
{{#include ../listings/ch08-generic-types-and-traits/no_listing_06_derive_generics/src/lib.cairo}}
Kode di atas menghasilkan trait Drop
untuk tipe Wallet
secara otomatis. Ini setara dengan menulis kode berikut:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_07_drop_explicit/src/lib.cairo}}
Kita menghindari menggunakan macro derive
untuk implementasi Drop
dari Wallet
dan sebaliknya menentukan implementasi WalletDrop
kita sendiri. Perhatikan bahwa kita harus mendefinisikan, seperti fungsi, jenis generik tambahan untuk WalletDrop
yang menyatakan bahwa T
juga mengimplementasikan trait Drop
. Kita basically mengatakan bahwa struct Wallet<T>
dapat dihapus selama T
juga dapat dihapus.
Terakhir, jika kita ingin menambahkan field ke Wallet
yang mewakili Addressnya dan kita ingin field tersebut berbeda dari T
tetapi juga generik, kita dapat dengan mudah menambahkan jenis generik lain di antara <>
:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_08_two_generics/src/lib.cairo}}
Kita menambahkan definisi jenis generik U
ke dalam struct Wallet
dan kemudian mengassign jenis ini ke member field address
yang baru. Perhatikan bahwa atribut derive
untuk trait Drop
juga berfungsi untuk U
.
Enums
Seperti yang kita lakukan dengan structs, kita dapat mendefinisikan enums untuk menyimpan jenis data generik dalam variasi mereka. Contohnya adalah enum Option<T>
yang disediakan oleh perpustakaan inti Cairo:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_09_option/src/lib.cairo}}
Enum Option<T>
adalah generik terhadap jenis T
dan memiliki dua variasi: Some
, yang menyimpan satu nilai bertipe T
, dan None
yang tidak menyimpan nilai apa pun. Dengan menggunakan enum Option<T>
, kita dapat menyatakan konsep abstrak dari nilai opsional dan karena nilai tersebut memiliki jenis generik T
, kita dapat menggunakan abstraksi ini dengan jenis apa pun.
Enums juga dapat menggunakan beberapa jenis generik, seperti definisi enum Result<T, E>
yang disediakan oleh perpustakaan inti:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_10_result/src/lib.cairo}}
Enum Result<T, E>
memiliki dua jenis generik, T
dan E
, dan dua variasi: Ok
yang menyimpan nilai bertipe T
dan Err
yang menyimpan nilai bertipe E
. Definisi ini memudahkan penggunaan enum Result
di mana pun kita memiliki operasi yang mungkin berhasil (dengan mengembalikan nilai bertipe T
) atau gagal (dengan mengembalikan nilai bertipe E
).
Metode Generik
Kita dapat mengimplementasikan metode pada structs dan enums, dan menggunakan jenis generik dalam definisinya juga. Dengan menggunakan definisi sebelumnya dari struct Wallet<T>
, kita mendefinisikan metode balance
untuknya:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_11_generic_methods/src/lib.cairo}}
Pertama, kita mendefinisikan trait WalletTrait<T>
menggunakan jenis generik T
yang mendefinisikan metode yang mengembalikan snapshot dari field balance
dari Wallet
. Kemudian kita memberikan implementasi untuk trait tersebut pada WalletImpl<T>
. Perhatikan bahwa Anda perlu menyertakan jenis generik di kedua definisi trait dan implementasi.
Kita juga dapat menentukan kendala pada jenis generik saat mendefinisikan metode pada jenis tersebut. Kita bisa, misalnya, mengimplementasikan metode hanya untuk instance Wallet<u128>
daripada Wallet<T>
. Pada contoh kode, kita mendefinisikan implementasi untuk Wallet yang memiliki tipe konkret u128
untuk field balance
.
{{#include ../listings/ch08-generic-types-and-traits/no_listing_12_constrained_generics/src/lib.cairo}}
Metode baru receive
menambah ukuran saldo dari setiap instance Wallet<u128>
. Perhatikan bahwa kita mengubah fungsi main
membuat w
menjadi variabel yang dapat diubah agar dapat memperbarui saldo. Jika kita mengubah inisialisasi w
dengan mengubah tipe balance
, kode sebelumnya tidak akan dikompilasi.
Cairo memungkinkan kita mendefinisikan metode generik di dalam trait generik juga. Menggunakan implementasi sebelumnya dari Wallet<U, V>
, kita akan mendefinisikan sebuah trait yang memilih dua Wallet dari jenis generik yang berbeda dan membuat yang baru dengan jenis generik masing-masing. Pertama, mari kita menulis ulang definisi struct:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_13_not_compiling/src/lib.cairo:struct}}
Selanjutnya, kita akan secara naif mendefinisikan trait dan implementasinya:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_13_not_compiling/src/lib.cairo:trait_impl}}
Kita membuat trait WalletMixTrait<T1, U1>
dengan metode mixup<T2, U2>
yang diberikan sebuah instance Wallet<T1, U1>
dan Wallet<T2, U2>
membuat sebuah Wallet<T1, U2>
baru. Seperti yang dijelaskan pada tanda tangan mixup
, baik self
maupun other
akan di-drop pada akhir fungsi, itulah alasan mengapa kode ini tidak akan dikompilasi. Jika Anda telah mengikuti dari awal hingga sekarang, Anda akan tahu bahwa kita harus menambahkan persyaratan untuk semua jenis generik tersebut, menentukan bahwa mereka akan mengimplementasikan trait Drop
agar kompiler tahu cara menjatuhkan instance Wallet<T, U>
. Implementasi yang diperbarui adalah sebagai berikut:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_14_compiling/src/lib.cairo:trait_impl}}
Kita menambahkan persyaratan untuk T1
dan U1
agar dapat di-drop pada deklarasi WalletMixImpl
. Kemudian kita lakukan hal yang sama untuk T2
dan U2
, kali ini sebagai bagian dari tanda tangan mixup
. Sekarang kita dapat mencoba fungsi mixup
:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_14_compiling/src/lib.cairo:main}}
Pertama, kita membuat dua instance: satu dari Wallet<bool, u128>
dan yang lainnya dari Wallet<felt252, u8>
. Kemudian, kita memanggil mixup
dan membuat instance baru Wallet<bool, u8>
.
Sifat-sifat di Cairo
Sebuah sifat menentukan serangkaian metode yang dapat diimplementasikan oleh suatu tipe. Metode-metode ini dapat dipanggil pada instansi dari tipe tersebut ketika sifat ini diimplementasikan. Sebuah sifat yang digabungkan dengan tipe generik menentukan fungsionalitas yang dimiliki oleh suatu tipe tertentu dan dapat dibagikan dengan tipe-tipe lain. Kita dapat menggunakan sifat untuk menentukan perilaku bersama secara abstrak. Kita dapat menggunakan batasan sifat untuk menentukan bahwa suatu tipe generik dapat menjadi tipe apa pun yang memiliki perilaku tertentu.
Catatan: Sifat serupa dengan fitur yang sering disebut antarmuka dalam bahasa pemrograman lain, meskipun dengan beberapa perbedaan.
Meskipun sifat dapat ditulis untuk tidak menerima tipe generik, mereka paling berguna ketika digunakan dengan tipe generik. Kita sudah membahas generik pada bab sebelumnya, dan kita akan menggunakannya dalam bab ini untuk menunjukkan bagaimana sifat dapat digunakan untuk menentukan perilaku bersama untuk tipe generik.
Menentukan Sifat
Perilaku suatu tipe terdiri dari metode-metode yang dapat kita panggil pada tipe tersebut. Tipe-tipe yang berbeda berbagi perilaku yang sama jika kita dapat memanggil metode yang sama pada semua tipe tersebut. Definisi sifat adalah cara untuk mengelompokkan tanda tangan metode bersama untuk menentukan serangkaian perilaku yang diperlukan untuk mencapai suatu tujuan.
Sebagai contoh, katakanlah kita memiliki sebuah struktur NewsArticle
yang menyimpan berita di lokasi tertentu. Kita dapat menentukan sifat Summary
yang menjelaskan perilaku sesuatu yang dapat merangkum tipe NewsArticle
.
{{#rustdoc_include ../listings/ch08-generic-types-and-traits/no_listing_14_simple_trait/src/lib.cairo:trait}}
Di sini, kita mendeklarasikan suatu sifat menggunakan kata kunci trait
dan kemudian nama sifatnya, yaitu Summary
dalam hal ini.
Di dalam kurung kurawal, kita mendeklarasikan tanda tangan metode yang menjelaskan perilaku dari tipe-tipe yang mengimplementasikan sifat ini, yang dalam hal ini adalah fn summarize(self: @NewsArticle) -> ByteArray
. Setelah tanda tangan metode, alih-alih memberikan implementasi di dalam kurung kurawal, kita menggunakan titik koma.
Catatan: Tipe
ByteArray
adalah tipe yang digunakan untuk merepresentasikan String di Cairo.
Karena sifat tidak bersifat generik, parameter self
juga tidak bersifat generik dan bertipe @NewsArticle
. Ini berarti bahwa metode summarize
hanya dapat dipanggil pada instansi NewsArticle
.
Sekarang, pertimbangkan bahwa kita ingin membuat kerangka perpustakaan pengumpul media yang dinamai aggregator
yang dapat menampilkan ringkasan data yang mungkin disimpan dalam instansi NewsArticle
atau Tweet
. Untuk melakukannya, kita memerlukan ringkasan dari setiap tipe, dan kita akan meminta ringkasan tersebut dengan memanggil metode summarize
pada suatu instansi. Dengan menentukan sifat Summary
pada tipe generik T
, kita dapat mengimplementasikan metode summarize
pada tipe apa pun yang ingin kita rangkum.
{{#rustdoc_include ../listings/ch08-generic-types-and-traits/no_listing_15_traits/src/lib.cairo:trait}}
Sebuah sifat Summary
yang terdiri dari perilaku yang diberikan oleh metode summarize
Setiap tipe generik yang mengimplementasikan sifat ini harus menyediakan perilaku kustomnya sendiri untuk tubuh metode. Kompiler akan menegakkan bahwa setiap tipe yang memiliki sifat Summary akan memiliki metode summarize yang didefinisikan dengan tanda tangan ini secara tepat.
Sebuah sifat dapat memiliki beberapa metode di dalam tubuhnya: tanda tangan metode terdaftar satu per baris dan setiap baris diakhiri dengan titik koma.
Mengimplementasikan Sifat pada Sebuah Tipe
Sekarang bahwa kita telah menentukan tanda tangan yang diinginkan dari metode-metode sifat Summary
,
kita dapat mengimplementasikannya pada tipe-tipe di pengumpul media kita. Potongan kode berikut menunjukkan
implementasi sifat Summary
pada struktur NewsArticle
yang menggunakan
judul, penulis, dan lokasi untuk membuat nilai kembalian dari
summarize
. Untuk struktur Tweet
, kita mendefinisikan summarize
sebagai nama pengguna
diikuti oleh seluruh teks tweet, dengan asumsi konten tweet
sudah terbatas pada 280 karakter.
{{#rustdoc_include ../listings/ch08-generic-types-and-traits/no_listing_15_traits/src/lib.cairo:impl}}
Mengimplementasikan sifat pada suatu tipe mirip dengan mengimplementasikan metode biasa. Perbedaannya adalah setelah impl
, kita menentukan nama implementasi,
kemudian menggunakan kata kunci of
, dan kemudian menentukan nama sifat untuk implementasi tersebut.
Jika implementasinya adalah untuk tipe generik, kita menempatkan nama tipe generik di dalam tanda kurung sudut setelah nama sifat.
Di dalam blok impl
, kita menempatkan tanda tangan metode
yang telah ditentukan oleh definisi sifat. Alih-alih menambahkan titik koma setelah setiap
tanda tangan, kita menggunakan kurung kurawal dan mengisi tubuh metode dengan perilaku spesifik
yang kita inginkan agar metode-metode sifat memiliki perilaku tertentu untuk tipe tertentu.
Sekarang bahwa perpustakaan telah mengimplementasikan sifat Summary
pada NewsArticle
dan
Tweet
, pengguna dari kerangka ini dapat memanggil metode sifat pada instansi
NewsArticle
dan Tweet
dengan cara yang sama seperti memanggil metode biasa. Satu-satunya
perbedaan adalah bahwa pengguna harus mengimpor sifat ke dalam lingkup serta tipe-tipe tersebut.
Berikut adalah contoh bagaimana kerangka dapat digunakan:
{{#rustdoc_include ../listings/ch08-generic-types-and-traits/no_listing_15_traits/src/lib.cairo:main}}
Kode ini mencetak keluar:
Berita baru tersedia! Cairo telah menjadi bahasa paling populer bagi pengembang oleh Cairo Digger (Seluruh Dunia)
1 tweet baru: EliBenSasson: Crypto is full of short-term maximizing projects.
@Starknet and @StarkWareLtd are about long-term vision maximization.
Kerangka lain yang bergantung pada kerangka aggregator
juga dapat mengimpor sifat Summary
ke dalam lingkup untuk mengimplementasikan Summary
pada tipe-tipe mereka sendiri.
Mengimplementasikan Sifat, Tanpa Menulis Deklarasinya.
Anda dapat menulis implementasi langsung tanpa mendefinisikan sifat yang sesuai. Ini dimungkinkan dengan menggunakan atribut #[generate_trait]
dalam implementasi, yang akan membuat kompiler secara otomatis menghasilkan sifat yang sesuai dengan implementasi tersebut. Ingatlah untuk menambahkan Trait
sebagai akhiran dari nama sifat Anda, karena kompiler akan membuat sifat tersebut dengan menambahkan akhiran Trait
ke nama implementasi.
{{#include ../listings/ch08-generic-types-and-traits/no_listing_16_generate_trait/src/lib.cairo}}
Dalam kode tersebut, tidak perlu mendefinisikan sifat secara manual. Kompiler akan menangani definisinya secara otomatis, secara dinamis menghasilkan dan memperbarui sifat saat fungsi-fungsi baru diperkenalkan.
Mengelola dan Menggunakan Implementasi Sifat Eksternal
Untuk menggunakan metode-metode sifat, Anda perlu memastikan bahwa sifat/implemetasi yang benar diimpor. Dalam kode di atas, kita mengimpor PrintTrait
dari debug
dengan use core::debug::PrintTrait;
untuk menggunakan metode print()
pada tipe-tipe yang didukung. Semua sifat yang termasuk dalam prelude tidak perlu diimpor secara eksplisit dan dapat diakses secara bebas.
Dalam beberapa kasus, Anda mungkin perlu mengimpor tidak hanya sifat tetapi juga implementasinya jika mereka dideklarasikan dalam modul terpisah.
Jika CircleGeometry
berada di modul/berkas terpisah circle
, maka untuk menggunakan boundary
pada circ: Circle
, kita perlu mengimpor CircleGeometry
selain ShapeGeometry
.
Jika kode diorganisir ke dalam modul seperti ini, di mana implementasi sifat didefinisikan dalam modul yang berbeda dengan sifat itu sendiri, mengimpor implementasi yang relevan secara eksplisit diperlukan.
use core::debug::PrintTrait;
// struct Circle { ... } and struct Rectangle { ... }
mod geometry {
use super::Rectangle;
trait ShapeGeometry<T> {
// ...
}
impl RectangleGeometry of ShapeGeometry<Rectangle> {
// ...
}
}
// Bisa berada di berkas yang berbeda
mod circle {
use super::geometry::ShapeGeometry;
use super::Circle;
impl CircleGeometry of ShapeGeometry<Circle> {
// ...
}
}
fn main() {
let rect = Rectangle { height: 5, width: 7 };
let circ = Circle { radius: 5 };
// Gagal dengan kesalahan ini
// Metode `area` tidak ditemukan pada... Apakah Anda mengimpor sifat dan impl yang benar?
rect.area().print();
circ.area().print();
}
Untuk membuatnya berfungsi, selain dari,
use geometry::ShapeGeometry;
Anda perlu mengimpor CircleGeometry
secara eksplisit. Perhatikan bahwa Anda tidak perlu mengimpor RectangleGeometry
, karena itu didefinisikan dalam modul yang sama dengan sifat yang diimpor, dan dengan demikian secara otomatis dipecahkan.
use circle::CircleGeometry
Ujicoba Program Cairo
Cara Menulis Uji Coba
Anatomi Fungsi Uji Coba
Uji coba adalah fungsi Cairo yang memverifikasi bahwa kode non-uji berfungsi sesuai dengan yang diharapkan. Tubuh fungsi uji coba biasanya melakukan tiga tindakan ini:
- Menyiapkan data atau keadaan yang diperlukan.
- Menjalankan kode yang ingin diuji.
- Memastikan hasil sesuai dengan yang diharapkan.
Mari kita lihat fitur-fitur yang disediakan Cairo khusus untuk menulis uji coba yang melibatkan tindakan-tindakan ini, termasuk atribut test
, fungsi assert
, dan atribut should_panic
.
Anatomi Fungsi Uji Coba
Pada dasarnya, uji coba di Cairo adalah fungsi yang dianotasi dengan atribut test
. Atribut adalah metadata tentang potongan kode Cairo; salah satu contohnya adalah atribut derive yang kita gunakan dengan struct di Bab 5. Untuk mengubah fungsi menjadi fungsi uji coba, tambahkan #[test]
pada baris sebelum fn
. Saat Anda menjalankan uji coba dengan perintah scarb cairo-test
, Scarb menjalankan binary pelari uji coba Cairo yang menjalankan fungsi-fungsi yang dianotasi dan melaporkan apakah setiap fungsi uji coba lulus atau gagal.
Mari buat proyek baru bernama adder
yang akan menambahkan dua angka menggunakan Scarb dengan perintah scarb new adder
:
adder
├── Scarb.toml
└── src
└── lib.cairo
Di lib.cairo, mari hapus konten yang ada dan tambahkan uji coba pertama, seperti yang ditunjukkan dalam Listing 9-1.
Nama file: src/lib.cairo
{{#include ../listings/ch09-testing-cairo-programs/listing_08_01_02/src/lib.cairo:it_works}}
Listing 9-1: Modul dan fungsi uji coba
Saat ini, mari abaikan dua baris teratas dan fokus pada fungsi. Perhatikan anotasi #[test]
: atribut ini menunjukkan bahwa ini adalah fungsi uji coba, sehingga pelari uji coba tahu untuk memperlakukan fungsi ini sebagai uji coba. Kita juga mungkin memiliki fungsi-fungsi non-uji coba di modul uji coba untuk membantu menyiapkan skenario umum atau melakukan operasi umum, jadi kita selalu perlu menunjukkan fungsi mana yang merupakan uji coba.
Tubuh fungsi contoh menggunakan fungsi assert
, yang berisi hasil penambahan 2 dan 2, sama dengan 4. Pernyataan ini berfungsi sebagai contoh format uji coba yang khas. Mari jalankan untuk melihat bahwa uji coba ini lulus.
Perintah scarb cairo-test
menjalankan semua uji coba yang ditemukan di proyek kita, seperti yang ditunjukkan dalam Listing 9-2.
$ scarb cairo-test
testing adder...
running 1 tests
test adder::lib::tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 filtered out;
Listing 9-2: Keluaran dari menjalankan uji coba
scarb cairo-test
mengompilasi dan menjalankan uji coba. Kami melihat baris running 1 tests
. Baris berikutnya menunjukkan nama fungsi uji coba, disebut it_works
, dan bahwa hasil dari menjalankan uji coba tersebut adalah ok
. Ringkasan keseluruhan test result: ok.
berarti bahwa semua uji coba berhasil, dan bagian yang membaca 1 passed; 0 failed
menjumlahkan jumlah uji coba yang berhasil atau gagal.
Mungkin untuk menandai uji coba sebagai diabaikan sehingga tidak berjalan dalam suatu instance tertentu; kita akan membahasnya dalam bagian Mengabaikan Beberapa Uji Coba Kecuali Diminta Secara Khusus lebih lanjut dalam bab ini. Karena kita belum melakukannya di sini, ringkasan menunjukkan 0 ignored
. Kami juga dapat menyertakan argumen ke perintah scarb cairo-test
untuk menjalankan hanya uji coba yang namanya cocok dengan string tertentu; ini disebut penyaringan dan kita akan membahasnya dalam bagian Menjalankan Uji Coba Tunggal. Kami juga belum menyaring uji coba yang dijalankan, sehingga akhir ringkasan menunjukkan 0 filtered out
.
Mari mulai menyesuaikan uji coba sesuai kebutuhan kita. Pertama, ubah nama fungsi it_works
menjadi nama yang berbeda, seperti eksplorasi
, seperti ini:
Nama file: src/lib.cairo
{{#include ../listings/ch09-testing-cairo-programs/listing_08_01_02/src/lib.cairo:eksplorasi}}
Kemudian jalankan scarb cairo-test
lagi. Keluaran sekarang menunjukkan eksplorasi
bukan it_works
:
$ scarb cairo-test
running 1 tests
test adder::lib::tests::eksplorasi ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 filtered out;
Sekarang kita akan menambahkan uji coba lain, tapi kali ini kita akan membuat uji coba yang gagal! Uji coba gagal ketika ada yang panic dalam fungsi uji coba. Setiap uji coba dijalankan dalam thread baru, dan ketika thread utama melihat bahwa thread uji coba telah mati, uji coba ditandai sebagai gagal. Masukkan uji coba baru sebagai fungsi bernama lainnya
, sehingga file src/lib.cairo Anda terlihat seperti Listing 9-3.
{{#include ../listings/ch09-testing-cairo-programs/listing_08_03/src/lib.cairo:lainnya}}
Listing 9-3: Menambahkan uji coba kedua yang akan gagal
$ scarb cairo-test
running 2 tests
test adder::lib::tests::eksplorasi ... ok
test adder::lib::tests::lainnya ... fail
failures:
adder::lib::tests::lainnya - panicked with [1725643816656041371866211894343434536761780588 ('Make this test fail'), ].
Error: test result: FAILED. 1 passed; 1 failed; 0 ignored
Listing 9-4: Hasil uji coba ketika satu uji coba lulus dan satu uji coba gagal
Daripada ok
, baris adder::lib::tests::lainnya
menunjukkan fail
. Bagian baru muncul di antara hasil individual dan ringkasan. Ini menampilkan alasan rinci untuk setiap kegagalan uji coba. Dalam kasus ini, kita mendapatkan detail bahwa lainnya
gagal karena panic dengan [1725643816656041371866211894343434536761780588 ('Make this test fail'), ]
di file src/lib.cairo.
Baris ringkasan ditampilkan di akhir: secara keseluruhan, hasil uji coba kita adalah FAILED
. Kami memiliki satu uji coba yang lulus dan satu uji coba yang gagal.
Sekarang setelah Anda melihat seperti apa hasil uji coba dalam skenario yang berbeda, mari kita lihat beberapa fungsi yang berguna dalam uji coba.
Memeriksa Hasil dengan Fungsi assert
Fungsi assert
, yang disediakan oleh Cairo, berguna ketika Anda ingin memastikan bahwa suatu kondisi dalam uji coba mengevaluasi menjadi true
. Kami memberikan fungsi assert
argumen pertama yang mengevaluasi menjadi Boolean. Jika nilai tersebut adalah true
, tidak ada yang terjadi dan uji coba lulus. Jika nilai tersebut adalah false
, fungsi assert memanggil panic()
untuk menyebabkan uji coba gagal dengan pesan yang kita tentukan sebagai argumen kedua fungsi assert
. Menggunakan fungsi assert
membantu kita memeriksa bahwa kode kita berfungsi sesuai yang diinginkan.
Pada Bab 5, Listing 5-15, kita menggunakan struktur Rectangle
dan metode can_hold
, yang diulang di Listing 9-5. Mari letakkan kode ini di file src/lib.cairo, lalu tulis beberapa uji coba untuknya menggunakan fungsi assert
.
Nama file: src/lib.cairo
{{#include ../listings/ch09-testing-cairo-programs/listing_08_06/src/lib.cairo:trait_impl}}
Listing 9-5: Menggunakan struktur Rectangle
dan metode can_hold
dari Bab 5
Metode can_hold
mengembalikan bool
, yang berarti ini adalah kasus penggunaan yang sempurna untuk fungsi assert. Pada Listing 9-6, kita menulis uji coba yang menggunakan metode can_hold
dengan membuat instans Rectangle
yang memiliki lebar 8
dan tinggi 7
dan menegaskan bahwa itu dapat menahan instans Rectangle
lain yang memiliki lebar 5
dan tinggi 1
.
Nama file: src/lib.cairo
{{#rustdoc_include ../listings/ch09-testing-cairo-programs/listing_08_06/src/lib.cairo:test1}}
Listing 9-6: Sebuah uji coba untuk can_hold
yang memeriksa apakah suatu persegi panjang yang lebih besar dapat benar-benar menahan persegi panjang yang lebih kecil
Perhatikan bahwa kita telah menambahkan dua baris baru di dalam modul uji coba: use super::Rectangle;
dan use super::RectangleTrait;
. Modul uji coba adalah modul biasa yang mengikuti aturan visibilitas biasa. Karena modul uji coba adalah modul dalam, kita perlu membawa kode yang diuji di modul luar ke dalam cakupan modul dalam.
Kami menamai uji coba kami larger_can_hold_smaller
, dan kami telah membuat dua instans Rectangle
yang kami butuhkan. Kemudian kami memanggil fungsi assert dan meneruskannya hasil dari pemanggilan larger.can_hold(@smaller)
. Ekspresi ini seharusnya mengembalikan true
, jadi uji coba kita seharusnya lulus. Mari kita cari tahu!
$ scarb cairo-test
running 1 tests
test adder::lib::tests::larger_can_hold_smaller ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 filtered out;
Itu berhasil! Mari tambahkan uji coba lain, kali ini menegaskan bahwa persegi panjang yang lebih kecil tidak dapat menahan persegi panjang yang lebih besar:
Nama file: src/lib.cairo
{{#rustdoc_include ../listings/ch09-testing-cairo-programs/listing_08_06/src/lib.cairo:test2}}
Karena hasil yang benar dari fungsi can_hold
dalam kasus ini adalah false
, kita perlu membalik hasil tersebut sebelum kami meneruskannya ke fungsi assert. Akibatnya, uji coba kita akan lulus jika can_hold
mengembalikan false:
$ scarb cairo-test
running 2 tests
test adder::lib::tests::smaller_cannot_hold_larger ... ok
test adder::lib::tests::larger_can_hold_smaller ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 filtered out;
Dua uji coba yang lulus! Sekarang mari lihat apa yang terjadi pada hasil uji coba kita ketika kita memperkenalkan bug dalam kode kita. Kami akan mengubah implementasi metode can_hold
dengan mengganti tanda lebih dari dengan tanda kurang dari saat membandingkan lebar:
{{#include ../listings/ch09-testing-cairo-programs/no_listing_02_wrong_can_hold_impl/src/lib.cairo:wrong_impl}}
Menjalankan uji coba sekarang menghasilkan hal berikut:
$ scarb cairo-test
running 2 tests
test adder::lib::tests::smaller_cannot_hold_larger ... ok
test adder::lib::tests::larger_can_hold_smaller ... fail
failures:
adder::lib::tests::larger_can_hold_smaller - panicked with [167190012635530104759003347567405866263038433127524 ('rectangle cannot hold'), ].
Error: test result: FAILED. 1 passed; 1 failed; 0 ignored
Uji coba kita menangkap bug tersebut! Karena larger.width
adalah 8
dan smaller.width
adalah 5
, perbandingan lebar dalam can_hold
sekarang mengembalikan false
: 8
tidak kurang dari 5
.
Memeriksa Panic dengan should_panic
Selain memeriksa nilai kembalian, penting untuk memastikan bahwa kode kita menangani kondisi kesalahan sebagaimana yang diharapkan. Sebagai contoh, pertimbangkan tipe Guess
pada Listing 9-8. Kode lain yang menggunakan Guess
bergantung pada jaminan bahwa instance Guess
hanya akan berisi nilai antara 1
dan 100
. Kita dapat menulis uji yang memastikan mencoba membuat instance Guess
dengan nilai di luar rentang tersebut menyebabkan panic.
Ini dapat dilakukan dengan menambahkan atribut should_panic
pada fungsi uji kita. Uji tersebut lulus jika kode di dalam fungsi menyebabkan panic; uji tersebut gagal jika kode di dalam fungsi tidak menyebabkan panic.
Listing 9-8 menunjukkan uji yang memeriksa bahwa kondisi kesalahan dari GuessTrait::new
terjadi seperti yang diharapkan.
Nama file: src/lib.cairo
{{#include ../listings/ch09-testing-cairo-programs/listing_08_08/src/lib.cairo}}
Listing 9-8: Menguji bahwa suatu kondisi akan menyebabkan panic
Kita menempatkan atribut #[should_panic]
setelah atribut #[test]
dan sebelum fungsi uji yang diterapkannya. Mari lihat hasilnya ketika uji ini berhasil:
$ scarb cairo-test
running 1 tests
test adder::lib::tests::greater_than_100 ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 filtered out;
Tampak bagus! Sekarang mari kita perkenalkan bug dalam kode kita dengan menghapus kondisi bahwa fungsi baru akan panic jika nilai lebih dari 100
:
{{#rustdoc_include ../listings/ch09-testing-cairo-programs/no_listing_03_wrong_new_impl/src/lib.cairo:here}}
Ketika kita menjalankan uji pada Listing 9-8, itu akan gagal:
$ scarb cairo-test
running 1 tests
test adder::lib::tests::greater_than_100 ... fail
failures:
adder::lib::tests::greater_than_100 - expected panic but finished successfully.
Error: test result: FAILED. 0 passed; 1 failed; 0 ignored
Kita tidak mendapatkan pesan yang sangat membantu dalam kasus ini, tetapi ketika kita melihat fungsi uji, kita melihat bahwa itu di-annotasi dengan #[should_panic]
. Kegagalan yang kita dapatkan berarti bahwa kode di dalam fungsi uji tidak menyebabkan panic.
Uji yang menggunakan should_panic
dapat tidak akurat. Uji should_panic
akan lulus bahkan jika uji menyebabkan panic karena alasan yang berbeda dari yang kita harapkan. Untuk membuat uji should_panic
lebih tepat, kita dapat menambahkan parameter opsional yang diharapkan ke atribut should_panic
. Harness uji akan memastikan bahwa pesan kegagalan berisi teks yang diberikan. Sebagai contoh, pertimbangkan kode yang dimodifikasi untuk Guess
pada Listing 9-9 di mana fungsi baru menyebabkan panic dengan pesan yang berbeda tergantung pada apakah nilai terlalu kecil atau terlalu besar.
Nama file: src/lib.cairo
{{#rustdoc_include ../listings/ch09-testing-cairo-programs/listing_08_09/src/lib.cairo:test_panic}}
Listing 9-9: Menguji panic dengan pesan panic yang berisi string pesan kesalahan
Uji ini akan lulus karena nilai yang kita masukkan pada parameter yang diharapkan atribut should_panic
adalah array string dari pesan bahwa fungsi Guess::new
panic. Kita perlu menentukan seluruh pesan panic yang kita harapkan.
Untuk melihat apa yang terjadi ketika uji should_panic
dengan pesan yang diharapkan gagal, mari kita lagi memasukkan bug ke dalam kode kita dengan menukar tubuh blok if value < 1
dan blok else if value > 100
:
{{#include ../listings/ch09-testing-cairo-programs/no_listing_04_new_bug/src/lib.cairo:here}}
Kali ini ketika kita menjalankan uji should_panic
, itu akan gagal:
$ scarb cairo-test
running 1 tests
test adder::lib::tests::greater_than_100 ... fail
failures:
adder::lib::tests::greater_than_100 - panicked with [6224920189561486601619856539731839409791025 ('Guess must be >= 1'), ].
Error: test result: FAILED. 0 passed; 1 failed; 0 ignored
Pesan kegagalan menunjukkan bahwa uji ini memang panic sesuai yang kita harapkan, tetapi pesan panic tidak menyertakan string yang diharapkan. Pesan panic yang kita dapatkan dalam kasus ini adalah Guess must be >= 1
. Sekarang kita dapat mulai mencari di mana letak bug kita!
Menjalankan Uji Tunggal
Terkadang, menjalankan seluruh rangkaian uji bisa memakan waktu lama. Jika Anda sedang bekerja pada kode di area tertentu, Anda mungkin hanya ingin menjalankan uji yang berkaitan dengan kode tersebut. Anda dapat memilih uji mana yang akan dijalankan dengan melewatkan opsi -f
(untuk "filter") kepada scarb cairo-test
, diikuti dengan nama uji yang ingin dijalankan sebagai argumen.
Untuk mendemonstrasikan cara menjalankan uji tunggal, pertama-tama kita akan membuat dua fungsi uji, seperti yang ditunjukkan pada Listing 9-10, dan memilih mana yang ingin dijalankan.
Nama file: src/lib.cairo
{{#include ../listings/ch09-testing-cairo-programs/listing_08_10/src/lib.cairo}}
Listing 9-10: Dua uji dengan dua nama berbeda
Kita dapat melewatkan nama fungsi uji apa pun kepada cairo-test
untuk menjalankan hanya uji itu menggunakan opsi -f
:
$ scarb cairo-test -f add_two_and_two
running 1 tests
test adder::lib::tests::add_two_and_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 1 filtered out;
Hanya uji dengan nama add_two_and_two
yang dijalankan; uji lainnya tidak cocok dengan nama tersebut. Output uji memberi tahu kita bahwa ada satu uji lagi yang tidak dijalankan dengan menampilkan 1 filtered out di akhir.
Kita juga dapat menentukan sebagian dari nama uji, dan semua uji yang namanya mengandung nilai tersebut akan dijalankan.
Mengabaikan Beberapa Uji Kecuali Diminta Secara Khusus
Terkadang beberapa uji tertentu dapat sangat memakan waktu untuk dieksekusi, sehingga Anda mungkin ingin mengeluarkannya selama sebagian besar jalankan scarb cairo-test
. Alih-alih menyebutkan sebagai argumen semua uji yang ingin dijalankan, Anda dapat memberi tanda uji yang memakan waktu menggunakan atribut ignore
untuk mengeluarkannya, seperti yang ditunjukkan di sini:
Nama file: src/lib.cairo
{{#include ../listings/ch09-testing-cairo-programs/no_listing_05_ignore_tests/src/lib.cairo}}
Setelah #[test]
, kita tambahkan baris #[ignore]
pada uji yang ingin kita eksklusikan. Sekarang ketika kita menjalankan uji kita, it_works
berjalan, tetapi expensive_test
tidak:
$ scarb cairo-test
running 2 tests
test adder::lib::tests::expensive_test ... ignored
test adder::lib::tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 1 ignored; 0 filtered out;
Fungsi expensive_test
dicatat sebagai diabaikan.
Ketika Anda sudah pada titik di mana masuk akal untuk memeriksa hasil uji yang diabaikan dan Anda memiliki waktu untuk menunggu hasilnya, Anda dapat menjalankan scarb cairo-test --include-ignored
untuk menjalankan semua uji apakah diabaikan atau tidak.
Menguji fungsi rekursif atau perulangan
Saat menguji fungsi rekursif atau perulangan, Anda harus memberikan jumlah gas maksimum yang dapat dikonsumsi oleh uji. Ini mencegah terjadinya perulangan tak terbatas atau penggunaan gas yang terlalu banyak, dan dapat membantu Anda melakukan pengukuran efisiensi implementasi Anda. Untuk melakukannya, Anda harus menambahkan atribut #[available_gas(<Number>)]
pada fungsi uji. Contoh berikut menunjukkan cara menggunakannya:
Nama file: src/lib.cairo
{{#include ../listings/ch09-testing-cairo-programs/no_listing_08_test_gas/src/lib.cairo}}
Benchmark penggunaan gas dari suatu operasi tertentu
Saat Anda ingin melakukan benchmark penggunaan gas dari suatu operasi tertentu, Anda dapat menggunakan pola berikut dalam fungsi uji Anda.
let initial = testing::get_available_gas();
gas::withdraw_gas().unwrap();
/// kode yang ingin kita benchmark.
(testing::get_available_gas() - x).print();
Contoh berikut menunjukkan cara menggunakannya untuk menguji fungsi gas dari fungsi sum_n
di atas.
{{#include ../listings/ch09-testing-cairo-programs/no_listing_09_benchmark_gas/src/lib.cairo}}
Nilai yang dicetak ketika menjalankan scarb cairo-test
adalah jumlah gas yang dikonsumsi oleh operasi yang di-benchmark.
$ scarb cairo-test
testing no_listing_09_benchmark_gas ...
running 1 tests
[DEBUG] (raw: 0x179f8
test no_listing_09_benchmark_gas::benchmark_sum_n_gas ... ok (gas usage est.: 98030)
test result: ok. 1 passed; 0 failed; 0 ignored; 0 filtered out;
Di sini, penggunaan gas dari fungsi sum_n
adalah 96760 (representasi desimal dari angka heksadesimal). Jumlah total yang dikonsumsi oleh uji sedikit lebih tinggi pada 98030, karena beberapa langkah ekstra diperlukan untuk menjalankan seluruh fungsi uji.
Pengujian Organisasi
Kita akan memikirkan tentang pengujian dalam dua kategori utama: pengujian unit dan pengujian integrasi. Pengujian unit bersifat kecil dan lebih terfokus, menguji satu modul pada satu waktu secara terisolasi, dan dapat menguji fungsi-fungsi pribadi. Meskipun Cairo belum mengimplementasikan konsep fungsi/lapangan publik/privat, tetapi adalah praktik yang baik untuk mulai mengorganisir kode Anda seolah-olah itu demikian. Pengujian integrasi menggunakan kode Anda dengan cara yang sama seperti kode eksternal lainnya, menggunakan hanya antarmuka publik dan mungkin melibatkan beberapa modul per pengujian.
Menulis kedua jenis pengujian ini penting untuk memastikan bahwa bagian-bagian dari perpustakaan Anda melakukan apa yang Anda harapkan, baik secara terpisah maupun bersama-sama.
Pengujian Unit
Tujuan dari pengujian unit adalah untuk menguji setiap unit kode secara terisolasi dari sisa kode untuk dengan cepat menentukan di mana kode bekerja atau tidak sesuai harapan. Anda akan menempatkan pengujian unit di direktori src
dalam setiap file dengan kode yang sedang diuji.
Konvensinya adalah membuat modul bernama tests
di setiap file untuk menyimpan fungsi-fungsi pengujian dan memberikan anotasi modul dengan cfg(test)
.
Modul Pengujian dan #[cfg(test)]
Anotasi #[cfg(test)]
pada modul pengujian memberi tahu Cairo untuk mengompilasi dan menjalankan kode pengujian hanya ketika Anda menjalankan scarb cairo-test
, bukan ketika Anda menjalankan cairo-run
. Ini menghemat waktu kompilasi saat Anda hanya ingin membangun perpustakaan dan menghemat ruang dalam artefak yang dikompilasi karena pengujian tidak disertakan. Anda akan melihat bahwa karena pengujian integrasi berada di direktori yang berbeda, mereka tidak memerlukan anotasi #[cfg(test)]
. Namun, karena pengujian unit berada di file yang sama dengan kode, Anda akan menggunakan #[cfg(test)]
untuk menentukan bahwa mereka tidak boleh disertakan dalam hasil yang dikompilasi.
Ingat bahwa ketika kita membuat proyek adder
baru di bagian pertama bab ini, kita menulis pengujian pertama ini:
{{#include ../listings/ch09-testing-cairo-programs/no_listing_06_cfg_attr/src/lib.cairo}}
Nama file: src/lib.cairo
Atribut cfg
singkatan dari konfigurasi dan memberi tahu Cairo bahwa item berikutnya hanya harus disertakan dengan opsi konfigurasi tertentu. Dalam hal ini, opsi konfigurasi adalah test
, yang disediakan oleh Cairo untuk mengompilasi dan menjalankan pengujian. Dengan menggunakan atribut cfg
, Cairo mengompilasi kode pengujian kita hanya jika kita secara aktif menjalankan pengujian dengan scarb cairo-test
. Ini mencakup fungsi bantu apa pun yang mungkin ada dalam modul ini, selain fungsi-fungsi yang dianotasi dengan #[test]
.
Pengujian Integrasi
Pengujian integrasi menggunakan perpustakaan Anda dengan cara yang sama seperti kode lainnya. Tujuannya adalah untuk menguji apakah banyak bagian dari perpustakaan Anda bekerja bersama dengan benar. Unit kode yang bekerja dengan benar secara independen bisa memiliki masalah saat diintegrasikan, jadi cakupan pengujian dari kode yang terintegrasi juga penting. Untuk membuat pengujian integrasi, pertama-tama Anda memerlukan direktori tests
.
Direktori tests
adder
├── Scarb.toml
├── src
│ ├── lib.cairo
│ ├── tests
│ │ └── integration_test.cairo
│ └── tests.cairo
{{#include ../listings/ch09-testing-cairo-programs/no_listing_07_integration_test/src/lib.cairo}}
Nama file: src/lib.cairo
#[cfg(tests)]
mod integration_tests;
Nama file: src/tests.cairo
Masukkan kode pada Listing 9-11 ke dalam file src/tests/integration_test.cairo:
{{#include ../listings/ch09-testing-cairo-programs/no_listing_07_integration_test/src/tests/integration_tests.cairo}}
Nama file: src/tests/integration_test.cairo
Kita perlu membawa fungsi-fungsi yang diuji ke dalam ruang lingkup setiap file pengujian. Untuk alasan itu, kami menambahkan use adder::it_adds_two
di bagian atas kode, yang tidak perlu kami lakukan dalam pengujian unit.
Kemudian, untuk menjalankan semua pengujian integrasi kita, kita hanya perlu menambahkan filter untuk hanya menjalankan pengujian yang path-nya mengandung "integration_tests".
$ scarb test -f integration_tests
Running cairo-test adder
testing adder ...
running 1 tests
test adder::tests::integration_tests::internal ... ok (gas usage est.: 3770)
test result: ok. 1 passed; 0 failed; 0 ignored; 0 filtered out;
Hasil dari pengujian sama dengan yang telah kita lihat sebelumnya: satu baris untuk setiap pengujian.
Penanganan Error
Pada bab ini, kita akan menjelajahi berbagai teknik penanganan kesalahan yang disediakan oleh Cairo, yang tidak hanya memungkinkan Anda untuk menangani potensi masalah dalam kode Anda, tetapi juga membuatnya lebih mudah untuk membuat program yang dapat beradaptasi dan mudah dipelihara. Dengan memeriksa pendekatan yang berbeda dalam mengelola kesalahan, seperti pemadanan pola dengan enum Result, menggunakan operator ? untuk penyebaran kesalahan yang lebih ergonomis, dan menggunakan metode unwrap atau expect untuk penanganan kesalahan yang dapat dipulihkan, Anda akan memperoleh pemahaman yang lebih dalam tentang fitur penanganan kesalahan Cairo. Konsep-konsep ini sangat penting untuk membangun aplikasi yang tangguh yang dapat menangani situasi yang tidak terduga secara efektif, memastikan bahwa kode Anda siap untuk produksi.
Kesalahan yang Tidak Dapat Dipulihkan dengan panic
Di Cairo, masalah yang tidak terduga dapat muncul selama eksekusi program, menghasilkan kesalahan waktu eksekusi. Meskipun fungsi panic dari pustaka inti tidak memberikan penyelesaian untuk kesalahan-kesalahan ini, namun ia mengakui keberadaan mereka dan mengakhiri program. Ada dua cara utama yang dapat memicu panic di Cairo: secara tidak sengaja, melalui tindakan yang menyebabkan kode menjadi panic (misalnya, mengakses array melebihi batasnya), atau sengaja, dengan memanggil fungsi panic.
Ketika panic terjadi, ini menyebabkan penghentian mendadak dari program. Fungsi panic
mengambil sebuah array sebagai argumen, yang dapat digunakan untuk memberikan pesan kesalahan dan melakukan proses unwind di mana semua variabel di-drop dan kamus-kamus diremas untuk memastikan keamanan program dalam menghentikan eksekusi dengan aman.
Berikut adalah contoh bagaimana kita dapat menggunakan panic
dari dalam sebuah program dan mengembalikan kode kesalahan 2
:
Nama File: src/lib.cairo
{{#include ../listings/ch10-error-handling/no_listing_01_panic/src/lib.cairo}}
Menjalankan program akan menghasilkan output berikut:
$ scarb cairo-run --available-gas=200000000
Program berhenti secara mendadak dengan [2 (''), ].
Seperti yang dapat Anda perhatikan dalam output, pernyataan cetak tidak pernah tercapai, karena program menghentikan eksekusi setelah menemui pernyataan panic
.
Pendekatan alternatif dan lebih idiomatik untuk panic di Cairo adalah menggunakan fungsi panic_with_felt252
. Fungsi ini berfungsi sebagai abstraksi dari proses mendefinisikan array dan sering lebih disukai karena ungkapan niatnya yang lebih jelas dan ringkas. Dengan menggunakan panic_with_felt252
, pengembang dapat memicu panic dalam satu baris dengan memberikan pesan kesalahan felt252 sebagai argumen, membuat kode lebih mudah dibaca dan mudah dipelihara.
Mari kita pertimbangkan sebuah contoh:
{{#include ../listings/ch10-error-handling/no_listing_02_with_felt252/src/lib.cairo}}
Menjalankan program ini akan menghasilkan pesan kesalahan yang sama seperti sebelumnya. Dalam hal ini, jika tidak ada kebutuhan akan sebuah array dan beberapa nilai yang harus dikembalikan dalam kesalahan, maka panic_with_felt252
menjadi alternatif yang lebih ringkas.
Notasi nopanic
Anda dapat menggunakan notasi nopanic
untuk menunjukkan bahwa sebuah fungsi tidak akan pernah panic. Hanya fungsi-fungsi nopanic
yang dapat dipanggil dalam fungsi yang dianotasi sebagai nopanic
.
Contoh:
{{#include ../listings/ch10-error-handling/no_listing_03_nopanic/src/lib.cairo}}
Contoh yang salah:
{{#include ../listings/ch10-error-handling/no_listing_04_nopanic_wrong/src/lib.cairo}}
Jika Anda menulis fungsi berikut yang mencakup pemanggilan fungsi yang mungkin panic, Anda akan mendapatkan pesan kesalahan berikut:
error: Fungsi dideklarasikan sebagai nopanic tetapi memanggil fungsi yang mungkin panic.
--> test.cairo:2:12
assert(1 == 1, 'what');
^****^
Fungsi dideklarasikan sebagai nopanic tetapi memanggil fungsi yang mungkin panic.
--> test.cairo:2:5
assert(1 == 1, 'what');
^********************^
Perhatikan bahwa ada dua fungsi yang mungkin panic di sini, yaitu assert dan equality.
Atribut panic_with
Anda dapat menggunakan atribut panic_with
untuk menandai sebuah fungsi yang mengembalikan Option
atau Result
. Atribut ini mengambil dua argumen, yaitu data yang dilewatkan sebagai alasan panic serta nama untuk sebuah fungsi pembungkus. Ini akan membuat sebuah pembungkus untuk fungsi yang Anda anotasi yang akan panic jika fungsi mengembalikan None
atau Err
, fungsi panic akan dipanggil dengan data yang diberikan.
Contoh:
{{#include ../listings/ch10-error-handling/no_listing_05_panic_with/src/lib.cairo}}
Menggunakan assert
Fungsi assert dari pustaka inti Cairo sebenarnya adalah fungsi utilitas berdasarkan panic. Ia memastikan bahwa suatu ekspresi boolean benar pada waktu eksekusi, dan jika tidak benar, ia memanggil fungsi panic dengan nilai kesalahan. Fungsi assert mengambil dua argumen: ekspresi boolean yang akan diverifikasi, dan nilai kesalahan. Nilai kesalahan ditentukan sebagai felt252, sehingga string yang dilewatkan harus dapat muat di dalam felt252.
Berikut adalah contoh penggunaannya:
{{#include ../listings/ch10-error-handling/no_listing_06_assert/src/lib.cairo}}
Kami melakukan assert di dalam fungsi utama bahwa my_number
bukan nol untuk memastikan bahwa kita tidak melakukan pembagian dengan 0.
Pada contoh ini, my_number
adalah nol sehingga asertion akan gagal, dan program akan panic
dengan string 'number is zero' (sebagai felt252) dan pembagian tidak akan tercapai.
Kesalahan yang Dapat Diperbaiki dengan Result
Sebagian besar kesalahan tidak cukup serius untuk memerlukan program berhenti sepenuhnya. Terkadang, ketika suatu fungsi gagal, itu disebabkan oleh suatu alasan yang dapat Anda interpretasikan dan tanggapi dengan mudah. Sebagai contoh, jika Anda mencoba menambahkan dua bilangan bulat besar dan operasinya melampaui batas nilai yang dapat direpresentasikan, Anda mungkin ingin mengembalikan kesalahan atau hasil yang dibungkus daripada menyebabkan perilaku yang tidak terdefinisi atau menghentikan proses.
Enum Result
Ingat dari “Tipe data generik” di Bab 8 bahwa enum Result
didefinisikan memiliki dua varian, Ok
dan Err
, sebagai berikut:
{{#include ../listings/ch10-error-handling/no_listing_07_result_enum/src/lib.cairo}}
Enum Result<T, E>
memiliki dua tipe generik, T
dan E
, dan dua varian: Ok
yang menyimpan nilai bertipe T
dan Err
yang menyimpan nilai bertipe E
. Definisi ini memudahkan penggunaan enum Result
di mana pun kita memiliki operasi yang mungkin berhasil (dengan mengembalikan nilai bertipe T
) atau gagal (dengan mengembalikan nilai bertipe E
).
Trait ResultTrait
Trait ResultTrait
menyediakan metode-metode untuk bekerja dengan enum Result<T, E>
, seperti membuka nilai, memeriksa apakah Result
adalah Ok
atau Err
, dan memicu panic dengan pesan kustom. Implementasi ResultTraitImpl
mendefinisikan logika dari metode-metode ini.
{{#include ../listings/ch10-error-handling/no_listing_08_result_trait/src/lib.cairo}}
Metode expect
dan unwrap
serupa dalam hal keduanya mencoba mengekstrak nilai bertipe T
dari Result<T, E>
ketika berada dalam varian Ok
. Jika Result
adalah Ok(x)
, keduanya mengembalikan nilai x
. Namun, perbedaan kunci antara kedua metode tersebut terletak pada perilaku mereka saat Result
berada dalam varian Err
. Metode expect
memungkinkan Anda menyediakan pesan kesalahan kustom (sebagai nilai felt252
) yang akan digunakan saat terjadi panic, memberi Anda lebih banyak kontrol dan konteks atas panic tersebut. Di sisi lain, metode unwrap
memicu panic dengan pesan kesalahan default, memberikan informasi yang lebih sedikit tentang penyebab panic.
Metode expect_err
dan unwrap_err
memiliki perilaku yang sama persis, tetapi kebalikannya. Jika Result
adalah Err(x)
, keduanya mengembalikan nilai x
. Namun, perbedaan kunci antara keduanya terletak pada kasus Result::Ok()
. Metode expect_err
memungkinkan Anda menyediakan pesan kesalahan kustom (sebagai nilai felt252
) yang akan digunakan saat terjadi panic, memberi Anda lebih banyak kontrol dan konteks atas panic tersebut. Di sisi lain, metode unwrap_err
memicu panic dengan pesan kesalahan default, memberikan informasi yang lebih sedikit tentang penyebab panic.
Seorang pembaca yang teliti mungkin telah memperhatikan <+Drop<T>>
dan <+Drop<E>>
dalam empat tanda tangan metode pertama. Sintaks ini mewakili kendala tipe generik dalam bahasa Cairo. Kendala ini menunjukkan bahwa fungsi terkait memerlukan implementasi dari trait Drop
untuk tipe generik T
dan E
masing-masing.
Terakhir, metode is_ok
dan is_err
adalah fungsi utilitas yang disediakan oleh trait ResultTrait
untuk memeriksa varian dari nilai enum Result
.
is_ok
mengambil cuplikan nilai Result<T, E>
dan mengembalikan true
jika Result
adalah varian Ok
, yang berarti operasi berhasil. Jika Result
adalah varian Err
, metode ini mengembalikan false
.
is_err
mengambil referensi ke nilai Result<T, E>
dan mengembalikan true
jika Result
adalah varian Err
, yang berarti operasi mengalami kesalahan. Jika Result
adalah varian Ok
, metode ini mengembalikan false
.
Metode-metode ini berguna ketika Anda ingin memeriksa keberhasilan atau kegagalan suatu operasi tanpa mengonsumsi nilai Result, memungkinkan Anda melakukan operasi tambahan atau membuat keputusan berdasarkan varian tanpa membungkusnya.
Anda dapat menemukan implementasi dari ResultTrait
di sini.
Selalu lebih mudah dipahami dengan contoh.
Lihatlah tanda tangan fungsi ini:
fn u128_overflowing_add(a: u128, b: u128) -> Result<u128, u128>;
Fungsi ini mengambil dua bilangan bulat u128, a dan b, dan mengembalikan Result<u128, u128>
di mana varian Ok
menyimpan jumlah jika penambahan tidak melampaui batas, dan varian Err
menyimpan nilai yang melampaui batas jika penambahan melampaui batas.
Sekarang, kita bisa menggunakan fungsi ini di tempat lain. Misalnya:
fn u128_checked_add(a: u128, b: u128) -> Option<u128> {
match u128_overflowing_add(a, b) {
Result::Ok(r) => Option::Some(r),
Result::Err(r) => Option::None,
}
}
Di sini, fungsi ini menerima dua bilangan bulat u128, a dan b, dan mengembalikan Option<u128>
. Ini menggunakan Result
yang dikembalikan oleh u128_overflowing_add
untuk menentukan keberhasilan atau kegagalan operasi penambahan. Ekspresi match memeriksa Result
dari u128_overflowing_add
. Jika hasilnya adalah Ok(r)
, itu mengembalikan Option::Some(r)
yang berisi jumlah. Jika hasilnya adalah Err(r)
, itu mengembalikan Option::None
untuk menunjukkan bahwa operasi telah gagal karena melampaui batas. Fungsi ini tidak memicu panic dalam kasus melampaui batas.
Mari kita lihat contoh lain yang menunjukkan penggunaan unwrap
.
{{#include ../listings/ch10-error-handling/listing_01/src/lib.cairo:function}}
Listing 10-1: Menggunakan tipe Result
Dalam contoh ini, fungsi parse_u8
mengambil integer felt252
dan mencoba mengonversinya menjadi integer u8
menggunakan metode try_into
. Jika berhasil, itu mengembalikan Result::Ok(value)
, jika tidak mengembalikan Result::Err('Invalid integer')
.
Dua kasus uji kita adalah:
{{#rustdoc_include ../listings/ch10-error-handling/listing_01/src/lib.cairo:tests}}
Yang pertama menguji konversi yang valid dari felt252
menjadi u8
, mengharapkan metode unwrap
tidak memicu panic. Fungsi uji kedua mencoba mengonversi nilai yang berada di luar rentang u8
, mengharapkan metode unwrap
memicu panic dengan pesan kesalahan 'Invalid integer'.
Kita juga bisa menggunakan atribut #[should_panic] di sini.
?
operator ?
Operator terakhir yang akan kita bahas adalah operator ?
. Operator ?
digunakan untuk penanganan kesalahan yang lebih idiomatik dan ringkas. Ketika Anda menggunakan operator ?
pada tipe Result
atau Option
, itu akan melakukan hal berikut:
- Jika nilai adalah
Result::Ok(x)
atauOption::Some(x)
, itu akan langsung mengembalikan nilai dalamnya, yaitux
. - Jika nilai adalah
Result::Err(e)
atauOption::None
, itu akan menyebarkan kesalahan atauNone
dengan segera keluar dari fungsi.
Operator ?
berguna ketika Anda ingin menangani kesalahan secara implisit dan membiarkan fungsi pemanggil mengatasinya.
Berikut adalah contoh penggunaannya.
{{#include ../listings/ch10-error-handling/listing_02/src/lib.cairo:function}}
Listing 10-1: Menggunakan operator ?
Fungsi do_something_with_parse_u8
mengambil nilai felt252
sebagai input dan memanggil parse_u8
. Operator ?
digunakan untuk menyebarkan kesalahan, jika ada, atau membuka kemasan nilai yang berhasil.
Dan dengan sedikit kasus uji:
{{#rustdoc_include ../listings/ch10-error-handling/listing_02/src/lib.cairo:tests}}
Konsol akan mencetak kesalahan "Invalid Integer".
Ringkasan
Kita melihat bahwa kesalahan yang dapat diperbaiki dapat ditangani di Cairo menggunakan enum Result, yang memiliki dua varian: Ok
dan Err
. Enum Result<T, E>
bersifat generik, dengan tipe T
dan E
mewakili nilai yang berhasil dan nilai kesalahan, secara berturut-turut. ResultTrait
menyediakan metode-metode untuk bekerja dengan Result<T, E>
, seperti membuka nilai, memeriksa apakah hasilnya adalah Ok
atau Err
, dan memicu panic dengan pesan kustom.
Untuk menangani kesalahan yang dapat diperbaiki, sebuah fungsi dapat mengembalikan tipe Result
dan menggunakan pola pencocokan untuk menangani keberhasilan atau kegagalan suatu operasi. Operator ?
dapat digunakan untuk menangani kesalahan secara implisit dengan menyebarkan kesalahan atau membuka kemasan nilai yang berhasil. Ini memungkinkan penanganan kesalahan yang lebih ringkas dan jelas, di mana pemanggil bertanggung jawab untuk mengelola kesalahan yang dibangkitkan oleh fungsi yang dipanggil.
Fitur Lanjutan
Sekarang, mari kita pelajari tentang fitur-fitur lanjutan yang ditawarkan oleh Cairo.
Overloading Operator
Overloading operator adalah fitur dalam beberapa bahasa pemrograman yang memungkinkan pengulangan penggunaan operator standar, seperti penambahan (+), pengurangan (-), perkalian (*), dan pembagian (/), untuk bekerja dengan tipe yang ditentukan pengguna. Ini dapat membuat sintaks kode lebih intuitif, dengan memungkinkan operasi pada tipe yang ditentukan pengguna diekspresikan dengan cara yang sama seperti operasi pada tipe primitif.
Dalam Cairo, overloading operator dicapai melalui implementasi trait tertentu. Setiap operator memiliki trait terkait, dan pengulangan operator melibatkan penyediaan implementasi trait tersebut untuk tipe kustom. Namun, penting untuk menggunakan overloading operator secara bijak. Penyalahgunaan dapat menyebabkan kebingungan, membuat kode lebih sulit dipelihara, misalnya ketika tidak ada makna semantik pada operator yang di-overload.
Pertimbangkan contoh di mana dua Potion
perlu digabungkan. Potion
memiliki dua bidang data, yaitu mana dan kesehatan. Menggabungkan dua Potion
seharusnya menambahkan bidang-bidang mereka masing-masing.
{{#include ../listings/ch11-advanced-features/no_listing_01_potions/src/lib.cairo}}
Dalam kode di atas, kita mengimplementasikan trait Add
untuk tipe Potion
. Fungsi add mengambil dua argumen: lhs
dan rhs
(kiri dan kanan). Isi fungsi mengembalikan instance Potion
baru, dengan nilai bidangnya merupakan kombinasi dari lhs
dan rhs
.
Seperti yang diilustrasikan dalam contoh tersebut, pengulangan operator memerlukan spesifikasi tipe konkret yang di-overload. Trait generik yang di-overload adalah Add<T>
, dan kita mendefinisikan implementasi konkret untuk tipe Potion
dengan Add<Potion>
.
Macros
Bahasa Cairo memiliki beberapa plugin yang memungkinkan pengembang menyederhanakan kode mereka. Mereka disebut inline_macros
dan merupakan cara penulisan kode yang menghasilkan kode lain. Dalam Cairo, hanya ada dua macros
: array![]
dan consteval_int!()
.
Mari kita mulai dengan array!
Kadang-kadang, kita perlu membuat array dengan nilai yang sudah diketahui pada saat kompilasi. Cara dasar untuk melakukannya redundan. Pertama, Anda akan mendeklarasikan array dan kemudian menambahkan setiap nilai satu per satu. array!
adalah cara yang lebih sederhana untuk melakukan tugas ini dengan menggabungkan dua langkah.
Pada saat kompilasi, kompiler akan membuat array dan menambahkan semua nilai yang dilewatkan ke makro array!
secara berurutan.
Tanpa array!
:
{{#include ../listings/ch11-advanced-features/no_listing_02_array_macro/src/lib.cairo:no_macro}}
Dengan array!
:
{{#include ../listings/ch11-advanced-features/no_listing_02_array_macro/src/lib.cairo:array_macro}}
consteval_int!
Dalam beberapa situasi, seorang pengembang mungkin perlu mendeklarasikan konstanta yang merupakan hasil dari perhitungan bilangan bulat. Untuk menghitung ekspresi konstan dan menggunakan hasilnya pada waktu kompilasi, diperlukan penggunaan makro consteval_int!
.
Berikut adalah contoh dari consteval_int!
:
const a: felt252 = consteval_int!(2 * 2 * 2);
Ini akan diinterpretasikan sebagai const a: felt252 = 8;
oleh kompiler.
Hash
Pada intinya, penghashan adalah proses mengubah data masukan (sering disebut sebagai pesan) dengan panjang apa pun menjadi nilai berukuran tetap, biasanya disebut "hash." Transformasi ini bersifat deterministik, artinya masukan yang sama akan selalu menghasilkan nilai hash yang sama. Fungsi hash adalah komponen mendasar dalam berbagai bidang, termasuk penyimpanan data, kriptografi, dan verifikasi integritas data - dan sering digunakan dalam pengembangan kontrak pintar, terutama saat bekerja dengan pohon Merkle.
Dalam bab ini, kami akan mempresentasikan dua fungsi hash yang diimplementasikan secara asli dalam perpustakaan Cairo: Poseidon
dan Pedersen
. Kami akan membahas kapan dan bagaimana menggunakannya, serta melihat contoh dengan program cairo.
Fungsi hash dalam Cairo
Perpustakaan inti Cairo menyediakan dua fungsi hash: Pedersen dan Poseidon.
Fungsi hash Pedersen adalah algoritma kriptografi yang bergantung pada kriptografi kurva eliptika. Fungsi-fungsi ini melakukan operasi pada titik-titik sepanjang kurva eliptika — pada dasarnya, melakukan perhitungan dengan lokasi-lokasi titik ini — yang mudah dilakukan ke satu arah dan sulit untuk dibatalkan. Kesulitan satu arah ini didasarkan pada Masalah Logaritma Diskrit Kurva Eliptika (ECDLP), yang merupakan masalah yang sangat sulit untuk dipecahkan dan memastikan keamanan fungsi hash. Kesulitan membalikkan operasi-operasi ini adalah yang membuat fungsi hash Pedersen aman dan dapat diandalkan untuk tujuan kriptografi.
Poseidon adalah keluarga fungsi hash yang dirancang untuk menjadi sangat efisien sebagai sirkuit aljabar. Desainnya sangat efisien untuk sistem bukti Tanpa Pengetahuan, termasuk STARKs (sehingga, Cairo). Poseidon menggunakan metode yang disebut 'konstruksi spons,' yang menyerap data dan mengubahnya dengan aman menggunakan proses yang dikenal sebagai permutasi Hades. Versi Cairo dari Poseidon didasarkan pada permutasi state dengan tiga elemen dengan parameter tertentu.
Kapan Menggunakan Mereka?
Pedersen adalah fungsi hash pertama yang digunakan di Starknet, dan masih digunakan untuk menghitung Address variabel dalam penyimpanan (misalnya, LegacyMap
menggunakan Pedersen untuk menghash kunci dari pemetaan penyimpanan di Starknet). Namun, karena Poseidon lebih murah dan lebih cepat daripada Pedersen saat bekerja dengan sistem bukti STARK, sekarang ini adalah fungsi hash yang direkomendasikan untuk digunakan dalam program Cairo.
Bekerja dengan Hash
Perpustakaan inti memudahkan penggunaan hash. Trait Hash
diimplementasikan untuk semua jenis yang dapat dikonversi menjadi felt252
, termasuk felt252
itu sendiri. Untuk jenis yang lebih kompleks seperti struct, menurunkan Hash
memungkinkan mereka di-hash dengan mudah menggunakan fungsi hash pilihan Anda - asalkan semua bidang struct itu sendiri dapat di-hash. Anda tidak dapat menurunkan trait Hash
pada struct yang berisi nilai yang tidak dapat di-hash, seperti Array<T>
atau Felt252Dict<T>
, bahkan jika T
itu sendiri dapat di-hash.
Trait Hash
disertai dengan HashStateTrait
yang mendefinisikan metode dasar untuk bekerja dengan hash. Mereka memungkinkan Anda untuk menginisialisasi status hash yang akan berisi nilai-nilai sementara hash setelah setiap aplikasi fungsi hash; memperbarui status hash, dan menyelesaikannya ketika perhitungan selesai. HashStateTrait
didefinisikan sebagai berikut:
{{#include ../listings/ch11-advanced-features/no_listing_03_hash_trait/src/lib.cairo:hashtrait}}
Untuk menggunakan hash dalam kode Anda, Anda harus mengimpor trait dan fungsi yang relevan. Pada contoh berikut, kami akan menunjukkan bagaimana menghash sebuah struct menggunakan fungsi hash Pedersen dan Poseidon.
Langkah pertama adalah menginisialisasi hash dengan PoseidonTrait::new() -> HashState
atau PedersenTrait::new(base: felt252) -> HashState
tergantung pada fungsi hash yang ingin kita gunakan. Kemudian status hash dapat diperbarui dengan fungsi update(self: HashState, value: felt252) -> HashState
atau update_with(self: S, value: T) -> S
sebanyak yang diperlukan. Kemudian fungsi finalize(self: HashState) -> felt252
dipanggil pada status hash dan mengembalikan nilai hash sebagai felt252
.
{{#include ../listings/ch11-advanced-features/no_listing_04_hash_pedersen/src/lib.cairo:import}}
{{#include ../listings/ch11-advanced-features/no_listing_04_hash_pedersen/src/lib.cairo:structure}}
Karena struct kami mendapatkan trait HashTrait, kita dapat memanggil fungsi sebagai berikut untuk hashing Poseidon:
{{#rustdoc_include ../listings/ch11-advanced-features/no_listing_04_hash_poseidon/src/lib.cairo:main}}
Dan sebagai berikut untuk hashing Pedersen:
{{#rustdoc_include ../listings/ch11-advanced-features/no_listing_04_hash_pedersen/src/lib.cairo:main}}
Hashing Lanjutan: Menghash Array dengan Poseidon
Mari kita lihat contoh penghashan fungsi yang berisi Span<felt252>
. Untuk menghash Span<felt252>
atau sebuah struct yang berisi Span<felt252>
, Anda dapat menggunakan fungsi bawaan dalam Poseidon poseidon_hash_span(mut span: Span<felt252>) -> felt252
. Demikian pula, Anda dapat menghash Array<felt252>
dengan memanggil poseidon_hash_span
pada span-nya.
Pertama, mari kita impor trait dan fungsi berikut:
{{#include ../listings/ch11-advanced-features/no_listing_05_advanced_hash/src/lib.cairo:import}}
Sekarang kita tentukan strukturnya, seperti yang mungkin Anda perhatikan, kita tidak mendapatkan trait Hash. Jika Anda mencoba mendapatkan trait Hash pada struktur ini, itu akan menyebabkan error karena struktur tersebut berisi suatu bidang yang tidak dapat di-hash.
{{#include ../listings/ch11-advanced-features/no_listing_05_advanced_hash/src/lib.cairo:structure}}
Dalam contoh ini, kita menginisialisasi HashState (hash
) dan memperbarui statusnya, kemudian memanggil fungsi finalize()
pada HashState untuk mendapatkan hash yang dihitung hash_felt252
. Kami menggunakan poseidon_hash_span
pada Span
dari Array<felt252>
untuk menghitung hash-nya.
{{#rustdoc_include ../listings/ch11-advanced-features/no_listing_05_advanced_hash/src/lib.cairo:main}}
Kontrak Pintar Starknet
Sepanjang bagian-bagian sebelumnya, Anda sebagian besar telah menulis program-program dengan main
sebagai titik masuk. Pada bagian-bagian mendatang, Anda akan belajar menulis dan mendeploy kontrak-kontrak Starknet.
Salah satu aplikasi dari bahasa Cairo adalah menulis kontrak pintar untuk jaringan Starknet. Starknet adalah jaringan tanpa izin yang memanfaatkan teknologi zk-STARKs untuk skalabilitas. Sebagai solusi skalabilitas Layer-2 untuk Ethereum, tujuan Starknet adalah menawarkan transaksi yang cepat, aman, dan berbiaya rendah. Ini berfungsi sebagai Validity Rollup (umumnya dikenal sebagai zero-knowledge Rollup) dan dibangun di atas bahasa Cairo dan Starknet VM.
Kontrak-kontrak Starknet, dengan kata-kata sederhana, adalah program-program yang dapat berjalan pada Starknet VM. Karena mereka berjalan di VM, mereka memiliki akses ke status persisten Starknet, dapat mengubah atau memodifikasi variabel-variabel dalam status Starknet, berkomunikasi dengan kontrak-kontrak lain, dan berinteraksi dengan mudah dengan L1 yang mendasarinya.
Kontrak-kontrak Starknet ditandai dengan atribut #[contract]
. Kami akan lebih mendalam tentang ini pada bagian-bagian selanjutnya.
Jika Anda ingin mempelajari lebih lanjut tentang jaringan Starknet itu sendiri, arsitekturnya, dan perangkat yang tersedia, Anda sebaiknya membaca Buku Starknet. Bagian ini akan fokus pada penulisan kontrak pintar di Cairo.
Scarb
Scarb mendukung pengembangan kontrak pintar untuk Starknet. Untuk mengaktifkan fungsionalitas ini, Anda perlu melakukan beberapa konfigurasi dalam file Scarb.toml
Anda (lihat Instalasi untuk cara menginstal Scarb).
Anda harus menambahkan dependensi starknet
dan menambahkan bagian [[target.starknet-contract]]
untuk mengaktifkan kompilasi kontrak.
Berikut adalah file Scarb.toml minimal yang diperlukan untuk mengompilasi crate yang berisi kontrak-kontrak Starknet:
[package]
name = "package_name"
version = "0.1.0"
[dependencies]
starknet = ">=2.4.0"
[[target.starknet-contract]]
Untuk konfigurasi tambahan, seperti dependensi kontrak eksternal, silakan merujuk ke Dokumentasi Scarb.
Setiap contoh dalam bab ini dapat digunakan dengan Scarb.
Pengantar tentang Kontrak Pintar
Bab ini akan memberikan pengantar tingkat tinggi tentang apa itu kontrak pintar, untuk apa mereka digunakan, dan mengapa pengembang blockchain menggunakan Cairo dan Starknet. Jika Anda sudah familiar dengan pemrograman blockchain, silakan lewati bab ini. Bagian terakhir mungkin tetap menarik.
Kontrak Pintar
Kontrak pintar menjadi populer dan semakin tersebar luas dengan lahirnya Ethereum. Kontrak pintar pada dasarnya adalah program-program yang diterapkan pada blockchain. Istilah "kontrak pintar" agak menyesatkan, karena mereka tidak benar-benar "pintar" atau "kontrak," melainkan kode dan instruksi yang dieksekusi berdasarkan input tertentu. Mereka terutama terdiri dari dua komponen: penyimpanan dan fungsi. Setelah diterapkan, pengguna dapat berinteraksi dengan kontrak pintar dengan memulai transaksi blockchain yang berisi data eksekusi (fungsi apa yang harus dipanggil dan dengan input apa). Kontrak pintar dapat memodifikasi dan membaca penyimpanan dari blockchain yang mendasarinya. Kontrak pintar memiliki Address sendiri dan dianggap sebagai akun blockchain, yang berarti dapat menyimpan token.
Bahasa pemrograman yang digunakan untuk menulis kontrak pintar bervariasi tergantung pada blockchain. Misalnya, di Ethereum dan ekosistem yang kompatibel dengan EVM, bahasa yang paling umum digunakan adalah Solidity, sedangkan di Starknet, menggunakan bahasa Cairo. Cara kode dikompilasi juga berbeda berdasarkan blockchain. Di Ethereum, Solidity dikompilasi menjadi bytecode. Di Starknet, Cairo dikompilasi menjadi Sierra, kemudian menjadi Cair Assembly (casm).
Kontrak pintar memiliki beberapa karakteristik unik. Mereka tanpa izin, artinya siapa pun dapat menerapkan kontrak pintar di jaringan (dalam konteks blockchain terdesentralisasi, tentu saja). Kontrak pintar juga transparan; data yang disimpan oleh kontrak pintar dapat diakses oleh siapa pun. Kode yang menyusun kontrak juga dapat transparan, memungkinkan komposabilitas. Ini memungkinkan pengembang menulis kontrak pintar yang menggunakan kontrak pintar lainnya. Kontrak pintar hanya dapat mengakses dan berinteraksi dengan data dari blockchain tempat mereka diterapkan. Mereka memerlukan perangkat lunak pihak ketiga (yang disebut oracle
) untuk mengakses data eksternal (seperti harga token, misalnya).
Untuk memungkinkan pengembang membangun kontrak pintar yang dapat berinteraksi satu sama lain, diperlukan pengetahuan tentang bagaimana kontrak lainnya terlihat. Oleh karena itu, pengembang Ethereum mulai membangun standar untuk pengembangan kontrak pintar, yang dikenal sebagai ERCxx
. Dua standar yang paling banyak digunakan dan terkenal adalah ERC20
, digunakan untuk membangun token seperti USDC
, DAI
, atau STARK
, dan ERC721
, untuk NFT (Token Non-fungible) seperti CryptoPunks
atau Everai
.
Kasus Penggunaan
Ada banyak kasus penggunaan potensial untuk kontrak pintar. Satu-satunya batasan adalah kendala teknis dari blockchain dan kreativitas para pengembang.
DeFi
Saat ini, penggunaan utama untuk kontrak pintar serupa dengan Ethereum atau Bitcoin, yaitu pada dasarnya menangani uang. Dalam konteks sistem pembayaran alternatif yang dijanjikan oleh Bitcoin, kontrak pintar di Ethereum memungkinkan pembuatan aplikasi keuangan terdesentralisasi yang tidak lagi bergantung pada perantara keuangan tradisional. Ini yang disebut sebagai DeFi (keuangan terdesentralisasi). DeFi terdiri dari berbagai proyek seperti aplikasi peminjaman/pinjaman, pertukaran terdesentralisasi (DEX), derivatif on-chain, stablecoin, dana lindung terdesentralisasi, asuransi, dan banyak lagi.
Tokenisasi
Kontrak pintar dapat memfasilitasi tokenisasi aset dunia nyata, seperti properti, seni, atau logam mulia. Tokenisasi membagi suatu aset menjadi token digital, yang dapat dengan mudah diperdagangkan dan dikelola di platform blockchain. Ini dapat meningkatkan likuiditas, memungkinkan kepemilikan fraksional, dan menyederhanakan proses jual beli.
Voting
Kontrak pintar dapat digunakan untuk membuat sistem pemilihan yang aman dan transparan. Suara dapat dicatat di blockchain, memastikan ketidakubahannya dan transparansi. Kontrak pintar kemudian dapat secara otomatis menghitung suara dan mengumumkan hasilnya, meminimalkan potensi kecurangan atau manipulasi.
Royalti
Kontrak pintar dapat mengotomatisasi pembayaran royalti bagi seniman, musisi, dan pencipta konten lainnya. Ketika suatu konten dikonsumsi atau dijual, kontrak pintar dapat secara otomatis menghitung dan mendistribusikan royalti kepada pemilik yang berhak, memastikan kompensasi yang adil dan mengurangi kebutuhan akan perantara.
Identitas Terdesentralisasi (DID)
Kontrak pintar dapat digunakan untuk membuat dan mengelola identitas digital, memungkinkan individu mengontrol informasi pribadi mereka dan berbagi dengan pihak ketiga secara aman. Kontrak pintar dapat memverifikasi otentisitas identitas pengguna dan secara otomatis memberikan atau mencabut akses ke layanan tertentu berdasarkan kredensial pengguna.
Seiring berlanjutnya kedewasaan Ethereum, kita dapat mengharapkan kasus penggunaan dan aplikasi kontrak pintar berkembang lebih jauh, membawa peluang baru yang menarik dan membentuk kembali sistem tradisional menjadi lebih baik.
Munculnya Starknet dan Cairo
Ethereum, sebagai platform kontrak pintar yang paling banyak digunakan dan tangguh, menjadi korban kesuksesannya sendiri. Dengan adopsi cepat beberapa kasus penggunaan yang telah disebutkan sebelumnya, terutama DeFi, biaya untuk melakukan transaksi menjadi sangat tinggi, membuat jaringan hampir tidak dapat digunakan. Insinyur dan peneliti di ekosistem ini mulai bekerja pada solusi untuk mengatasi masalah skalabilitas ini.
Trilema terkenal (The Blockchain Trilemma) dalam ruang blockchain menyatakan bahwa tidak mungkin mencapai tingkat skalabilitas, desentralisasi, dan keamanan yang tinggi secara bersamaan; harus ada kompromi. Ethereum berada pada persimpangan desentralisasi dan keamanan. Akhirnya, diputuskan bahwa tujuan Ethereum akan menjadi sebagai lapisan penyelesaian yang aman, sementara perhitungan kompleks akan dipindahkan ke jaringan lain yang dibangun di atas Ethereum. Ini disebut sebagai Layer 2 (L2).
Dua jenis utama dari L2 adalah optimistic rollups dan validity rollups. Kedua pendekatan melibatkan kompresi dan pengelompokan sejumlah transaksi bersama-sama, menghitung status baru, dan menyelesaikan hasilnya di Ethereum (L1). Perbedaannya terletak pada cara hasilnya diselesaikan di L1. Untuk optimistic rollups, status baru dianggap valid secara default, tetapi ada jendela 7 hari bagi node untuk mengidentifikasi transaksi yang bersifat berbahaya.
Sebaliknya, validity rollups, seperti Starknet, menggunakan kriptografi untuk membuktikan bahwa status baru telah dihitung dengan benar. Ini adalah tujuan dari STARKs, teknologi kriptografi ini dapat memungkinkan validity rollups untuk dapat meningkatkan skalabilitas secara signifikan lebih dari optimistic rollups. Anda dapat mempelajari lebih lanjut tentang STARKs dari artikel Medium Starkware, yang berfungsi sebagai panduan yang baik.
Arsitektur Starknet dijelaskan secara rinci dalam Buku Starknet, yang merupakan sumber yang bagus untuk mempelajari lebih lanjut tentang jaringan Starknet.
Ingat Cairo? Sebenarnya, ini adalah bahasa yang dikembangkan khusus untuk bekerja dengan STARKs dan membuatnya menjadi tujuan umum. Dengan Cairo, kita dapat menulis kode yang dapat dibuktikan. Dalam konteks Starknet, ini memungkinkan membuktikan kebenaran perhitungan dari satu status ke status lainnya.
Berbeda dengan sebagian besar (jika tidak semua) pesaing Starknet yang memilih menggunakan EVM (entah itu apa adanya atau disesuaikan) sebagai lapisan dasar, Starknet menggunakan VM sendiri. Ini membebaskan pengembang dari batasan EVM, membuka berbagai kemungkinan. Bersamaan dengan penurunan biaya transaksi, kombinasi Starknet dan Cairo menciptakan tempat bermain yang menarik bagi para pengembang. Abstraksi akun asli memungkinkan logika yang lebih kompleks untuk akun, yang disebut "Smart Accounts," dan alur transaksi. Kasus penggunaan yang muncul termasuk kecerdasan buatan yang transparan dan aplikasi pembelajaran mesin. Akhirnya, game blockchain dapat dikembangkan sepenuhnya on-chain. Starknet telah dirancang khusus untuk memaksimalkan kemampuan bukti STARK untuk skalabilitas yang optimal.
Pelajari lebih lanjut tentang Abstraksi Akun di Buku Starknet.
Program Cairo dan Kontrak Starknet: Apa Perbedaannya?
Kontrak Starknet adalah superset khusus dari program Cairo, sehingga konsep-konsep yang telah dipelajari sebelumnya dalam buku ini masih berlaku untuk menulis kontrak Starknet.
Seperti yang mungkin telah Anda perhatikan, program Cairo harus selalu memiliki fungsi main
yang berfungsi sebagai titik masuk untuk program ini:
fn main() {}
Kontrak Starknet pada dasarnya adalah program-program yang dapat berjalan di sistem operasi Starknet, dan sebagai program semacam itu, mereka memiliki akses ke status Starknet. Agar suatu modul dapat dianggap sebagai kontrak oleh kompiler, modul tersebut harus dianotasi dengan atribut #[starknet::contract]
.
Kontrak Sederhana
Bab ini akan memperkenalkan dasar-dasar kontrak Starknet dengan contoh kontrak dasar. Anda akan belajar bagaimana menulis kontrak sederhana yang menyimpan sebuah angka tunggal di blockchain.
Anatomi dari Kontrak Starknet Sederhana
Mari pertimbangkan kontrak berikut untuk menjelaskan dasar-dasar kontrak Starknet. Mungkin tidak mudah untuk memahaminya sekaligus, tetapi kita akan menjelaskannya langkah demi langkah:
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_01/src/lib.cairo:all}}
Listing 99-1: Kontrak penyimpanan sederhana
Catatan: Kontrak Starknet didefinisikan dalam modul.
Apa maksudnya kontrak ini?
Pada contoh ini, Storage
struct mendeklarasikan variabel penyimpanan yang disebut stored_data
dengan tipe u128
(bilangan bulat tak bertanda 128 bit).
Anda dapat memandangnya sebagai sebuah slot tunggal dalam sebuah database yang dapat Anda panggil dan ubah dengan memanggil fungsi dari kode yang mengelola database tersebut.
Kontrak ini mendefinisikan dan mengekspos secara publik fungsi-fungsi set
dan get
yang dapat digunakan untuk memodifikasi atau mengambil nilai dari variabel tersebut.
Antarmuka: desain kontrak
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_01/src/lib.cairo:interface}}
Antarmuka kontrak mewakili fungsi-fungsi yang kontrak ini tawarkan kepada dunia luar. Di sini, antarmuka mengekspos dua fungsi: set
dan get
. Dengan memanfaatkan mekanisme traits & impls dari Cairo, kita dapat memastikan bahwa implementasi sebenarnya dari kontrak sesuai dengan antarmukanya. Bahkan, Anda akan mendapatkan kesalahan kompilasi jika kontrak Anda tidak sesuai dengan antarmuka yang dinyatakan.
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_01_bis_wrong_impl/src/lib.cairo:impl}}
Listing 99-1-bis: Implementasi yang salah dari antarmuka kontrak. Ini tidak dapat dikompilasi.
Pada antarmuka, perhatikan tipe generik TContractState
dari argumen self
yang dilewatkan secara referensi ke fungsi set
. Parameter self
mewakili status kontrak. Melihat argumen self
yang dilewatkan ke set
memberi tahu kita bahwa fungsi ini mungkin mengakses status kontrak, karena itulah yang memberi kita akses ke penyimpanan kontrak. Modifikator ref
mengimplikasikan bahwa self
dapat dimodifikasi, yang berarti variabel penyimpanan kontrak dapat dimodifikasi di dalam fungsi set
.
Di sisi lain, get
mengambil snapshot dari TContractState
, yang segera memberi tahu kita bahwa ini tidak mengubah status (dan memang, kompiler akan mengeluh jika kita mencoba mengubah penyimpanan di dalam fungsi get
).
Fungsi publik didefinisikan dalam blok implementasi
Sebelum kita menjelajahi lebih jauh, mari tentukan beberapa terminologi.
-
Dalam konteks Starknet, fungsi publik adalah fungsi yang diakses oleh dunia luar. Dalam contoh di atas,
set
danget
adalah fungsi publik. Fungsi publik dapat dipanggil oleh siapa pun, dan dapat dipanggil dari luar kontrak atau dari dalam kontrak. Dalam contoh di atas,set
danget
adalah fungsi publik. -
Apa yang kita sebut sebagai fungsi eksternal adalah fungsi publik yang dipanggil melalui transaksi dan dapat mengubah status kontrak.
set
adalah fungsi eksternal. -
Fungsi view adalah fungsi publik yang dapat dipanggil dari luar kontrak, tetapi tidak dapat mengubah status kontrak.
get
adalah fungsi view.
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_01/src/lib.cairo:impl}}
Karena antarmuka kontrak didefinisikan sebagai trait ISimpleStorage
, untuk sesuai dengan antarmuka, fungsi eksternal kontrak
harus didefinisikan dalam implementasi dari trait ini — yang memungkinkan kita memastikan bahwa implementasi kontrak sesuai dengan antarmukanya.
Namun, hanya mendefinisikan fungsi-fungsi dalam implementasi belum cukup. Blok implementasi harus dianotasi dengan atribut #[external(v0)]
. Atribut ini mengekspos fungsi-fungsi yang didefinisikan dalam implementasi ini ke dunia luar — lupakan untuk menambahkannya dan fungsi-fungsi Anda tidak dapat dipanggil dari luar. Semua fungsi yang didefinisikan dalam blok yang ditandai dengan #[external(v0)]
secara konsekuensial adalah fungsi publik.
Saat menulis implementasi antarmuka, parameter generik yang sesuai dengan argumen self
dalam trait harus ContractState
. Tipe ContractState
dihasilkan oleh kompiler, dan memberikan akses ke variabel penyimpanan yang didefinisikan dalam struktur Storage
.
Selain itu, ContractState
memberikan kita kemampuan untuk menghasilkan acara (events). Nama ContractState
tidak mengherankan, karena ini adalah representasi dari status kontrak, yang merupakan apa yang kita pikirkan sebagai self
dalam trait antarmuka kontrak.
Memodifikasi Status Kontrak
Seperti yang dapat Anda perhatikan, semua fungsi yang perlu mengakses status kontrak didefinisikan dalam implementasi dari suatu trait yang memiliki parameter generik TContractState
, dan mengambil parameter self: ContractState
.
Ini memungkinkan kita secara eksplisit melewatkan parameter self: ContractState
ke fungsi, memungkinkan akses ke variabel penyimpanan kontrak.
Untuk mengakses variabel penyimpanan dari kontrak saat ini, Anda menambahkan awalan self
ke nama variabel penyimpanan, yang memungkinkan Anda menggunakan metode read
dan write
untuk membaca atau menulis nilai variabel penyimpanan.
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_01/src/lib.cairo:write_state}}
Menggunakan self
dan metode write
untuk memodifikasi nilai variabel penyimpanan
Catatan: jika status kontrak dilewatkan sebagai snapshot daripada
ref
, upaya untuk memodifikasinya akan menghasilkan kesalahan kompilasi.
Kontrak ini belum melakukan banyak hal selain memungkinkan siapa pun menyimpan satu angka yang dapat diakses oleh siapa pun di dunia. Siapa pun bisa memanggil set
lagi dengan nilai yang berbeda dan menimpa angka Anda, tetapi angka tersebut masih tersimpan dalam riwayat blockchain. Nanti, Anda akan melihat bagaimana Anda dapat memberlakukan pembatasan akses agar hanya Anda yang dapat mengubah angka tersebut.
Pemahaman yang Lebih Mendalam tentang Kontrak
Pada bagian sebelumnya, kami memberikan contoh pengantar tentang kontrak pintar yang ditulis dalam bahasa Cairo. Pada bagian ini, kita akan melihat lebih mendalam semua komponen dari kontrak pintar, langkah demi langkah.
Ketika kita membahas interfaces, kami menjelaskan perbedaan antara public functions, external functions, dan view functions, dan kami menyebutkan bagaimana berinteraksi dengan storage.
Pada titik ini, seharusnya ada beberapa pertanyaan yang muncul di pikiran Anda:
- Bagaimana cara saya mendefinisikan fungsi internal atau privat?
- Bagaimana cara saya mengeluarkan peristiwa (events)? Bagaimana cara saya membuat indeks untuk mereka?
- Di mana seharusnya saya mendefinisikan fungsi yang tidak perlu mengakses status kontrak?
- Adakah cara untuk mengurangi kode boilerplate?
- Bagaimana cara menyimpan jenis data yang lebih kompleks?
Untungnya, kita akan menjawab semua pertanyaan ini dalam bab ini. Mari pertimbangkan contoh kontrak berikut yang akan kita gunakan sepanjang bab ini:
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:all}}
Listing 99-1bis: Kontrak referensi kita untuk bab ini
Storage Kontrak
Cara paling umum untuk berinteraksi dengan Storage kontrak adalah melalui variabel Storage. Seperti yang disebutkan sebelumnya, variabel Storage memungkinkan Anda menyimpan data yang akan disimpan di Storage kontrak yang sendiri disimpan di blockchain. Data ini persisten dan dapat diakses serta dimodifikasi kapan saja setelah kontrak diimplementasikan.
Variabel Storage dalam kontrak Starknet disimpan dalam struktur khusus yang disebut Storage
:
{{#rustdoc_include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:storage}}
Sebuah Struktur Storage
Struktur Storage adalah struktur seperti struktur lainnya,
kecuali bahwa ini harus diberi anotasi #[storage]
. Anotasi ini memberi tahu kompiler untuk menghasilkan kode yang diperlukan untuk berinteraksi dengan status blockchain, dan memungkinkan Anda membaca dan menulis data dari dan ke Storage. Selain itu, ini memungkinkan Anda mendefinisikan pemetaan Storage menggunakan tipe LegacyMap
.
Setiap variabel yang disimpan dalam struktur Storage disimpan di lokasi yang berbeda dalam Storage kontrak. Address Storage variabel ditentukan oleh nama variabel, dan kunci eventual dari variabel jika itu adalah pemetaan.
Address Storage
Address variabel Storage dihitung sebagai berikut:
- Jika variabel adalah nilai tunggal (bukan pemetaan), Addressnya adalah hash
sn_keccak
dari pengkodean ASCII nama variabel.sn_keccak
adalah versi Starknet dari fungsi hash Keccak256, yang keluarannya dipotong menjadi 250 bit. - Jika variabel adalah pemetaan, Address nilai pada kunci
k_1,...,k_n
adalahh(...h(h(sn_keccak(nama_variabel),k_1),k_2),...,k_n)
di mana ℎ adalah hash Pedersen dan nilai akhir diambilmod (2^251) − 256
. - Jika ini adalah pemetaan ke nilai-nilai kompleks (misalnya, tuple atau struktur), maka nilai kompleks ini terletak dalam segmen yang berkelanjutan dimulai dari Address yang dihitung pada poin sebelumnya. Perlu dicatat bahwa 256 elemen lapangan adalah batasan saat ini pada ukuran maksimal nilai Storage kompleks.
Anda dapat mengakses Address variabel Storage dengan memanggil fungsi address
pada variabel, yang mengembalikan nilai StorageBaseAddress
.
{{#rustdoc_include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:owner_address}}
Berinteraksi dengan Variabel Storage
Variabel yang disimpan dalam struktur Storage dapat diakses dan dimodifikasi menggunakan fungsi read
dan write
, dan Anda dapat mendapatkan Address mereka di Storage menggunakan fungsi addr
. Fungsi-fungsi ini secara otomatis dihasilkan oleh kompiler untuk setiap variabel Storage.
Untuk membaca nilai dari variabel Storage owner
, yang merupakan nilai tunggal, kita memanggil fungsi read
pada variabel owner
, tanpa parameter.
{{#rustdoc_include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:read_owner}}
Memanggil fungsi read
pada variabel owner
Untuk membaca nilai dari variabel Storage names
, yang merupakan pemetaan dari ContractAddress
ke felt252
, kita memanggil fungsi read
pada variabel names
, dengan memberikan kunci address
sebagai parameter. Jika pemetaan memiliki lebih dari satu kunci, kita akan memberikan kunci-kunci lain sebagai parameter juga.
{{#rustdoc_include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:read}}
Memanggil fungsi read
pada variabel names
Untuk menulis nilai ke variabel Storage, kita memanggil fungsi write
dengan memberikan kunci-kunci akhir nilai sebagai argumen. Seperti halnya dengan fungsi read
, jumlah argumen tergantung pada jumlah kunci - di sini, kita hanya memberikan nilai untuk ditulis ke variabel owner
karena itu adalah variabel sederhana.
{{#rustdoc_include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:write_owner}}
Memanggil fungsi write
pada variabel owner
{{#rustdoc_include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:write}}
Memanggil fungsi write
pada variabel names
Menyimpan Tipe Kustom
Trait Store
, yang didefinisikan dalam modul starknet::storage_access
, digunakan untuk menentukan bagaimana suatu tipe harus disimpan dalam Storage. Agar suatu tipe dapat disimpan dalam Storage, ia harus mengimplementasikan trait Store
. Sebagian besar tipe dari pustaka inti, seperti bilangan bulat tidak bertanda (u8
, u128
, u256
, ...), felt252
, bool
, ContractAddress
, dll. mengimplementasikan trait Store
dan dapat disimpan tanpa tindakan lebih lanjut.
Tetapi bagaimana jika Anda ingin menyimpan tipe yang Anda tentukan sendiri, seperti enum atau struct? Dalam hal ini, Anda harus secara eksplisit memberi tahu kompiler cara menyimpan tipe ini.
Dalam contoh kita, kita ingin menyimpan struct Person
dalam Storage, yang dapat dilakukan dengan mengimplementasikan trait Store
untuk tipe Person
. Ini dapat dicapai dengan menambahkan atribut #[derive(starknet::Store)]
di atas definisi struct kita.
{{#rustdoc_include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:person}}
Demikian pula, Enums dapat ditulis ke Storage jika mereka mengimplementasikan trait Store
, yang dapat dengan mudah di-derive selama semua tipe terkait mengimplementasikan trait Store
.
{{#rustdoc_include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:enum_store}}
Tata Letak Storage Structs
Pada Starknet, structs disimpan dalam Storage sebagai urutan tipe primitif. Elemen-elemen struct disimpan dalam urutan yang sama seperti yang didefinisikan dalam definisi struct. Elemen pertama dari struct disimpan pada Address dasar struct, yang dihitung seperti yang dijelaskan dalam Address Storage dan dapat diperoleh dengan memanggil var.address()
, dan elemen-elemen berikutnya disimpan pada Address yang berdekatan dengan elemen pertama. Sebagai contoh, tata letak Storage untuk variabel owner
bertipe Person
akan menghasilkan tata letak berikut:
Kolom | Address |
---|---|
nama | owner.address() |
Address | owner.address() +1 |
Tata Letak Storage Enums
Ketika Anda menyimpan suatu varian enum, pada dasarnya yang Anda simpan adalah indeks varian dan nilai terkait yang mungkin. Indeks ini dimulai dari 0 untuk varian pertama enum Anda dan bertambah 1 untuk setiap varian berikutnya. Jika varian Anda memiliki nilai terkait, itu disimpan mulai dari Address yang segera mengikuti Address dasar. Sebagai contoh, misalkan kita memiliki enum RegistrationType
dengan varian finite
, yang membawa tanggal batas terkait. Tata letak Storagenya akan terlihat seperti ini:
Elemen | Address |
---|---|
Indeks varian (misalnya 1 untuk finite) | registration_type.address() |
Tanggal batas terkait | registration_type.address() + 1 |
Pemetaan Storage
Pemetaan Storage mirip dengan tabel hash karena memungkinkan pemetaan kunci ke nilai. Namun, tidak seperti tabel hash biasa, data kunci itu sendiri tidak disimpan - hanya hash-nya yang digunakan untuk mencari nilai terkait dalam Storage kontrak. Pemetaan tidak memiliki konsep panjang atau apakah sepasang kunci/nilai diatur. Satu-satunya cara untuk menghapus pemetaan adalah dengan mengatur nilainya ke nilai default nol.
Pemetaan hanya digunakan untuk menghitung lokasi data dalam Storage suatu kontrak dengan memberikan kunci tertentu. Oleh karena itu, mereka hanya diperbolehkan sebagai variabel Storage. Mereka tidak dapat digunakan sebagai parameter atau parameter pengembalian fungsi kontrak, dan tidak dapat digunakan sebagai tipe dalam struct.

Untuk mendeklarasikan pemetaan, gunakan tipe LegacyMap
yang diapit dalam tanda kurung sudut <>
,
menentukan tipe kunci dan nilai.
Anda juga dapat membuat pemetaan yang lebih kompleks dengan beberapa kunci. Anda dapat menemukannya dalam Listing 99-2bis seperti variabel Storage allowances
yang populer dalam Standar ERC20 yang memetakan owner
dan spender
yang diizinkan ke jumlah allowance
menggunakan beberapa kunci yang dilewatkan dalam tupel:
{{#include ../listings/ch99-starknet-smart-contracts/no_listing_01_storage_mapping/src/lib.cairo:here}}
Listing 99-2bis: Menyimpan pemetaan
Address dalam Storage dari suatu variabel yang disimpan dalam pemetaan dihitung sesuai dengan deskripsi dalam bagian Address Storage.
Jika kunci pemetaan adalah struct, setiap elemen struct menjadi kunci. Selain itu, struct harus mengimplementasikan trait Hash
, yang dapat diperoleh dengan atribut #[derive(Hash)]
. Sebagai contoh, jika Anda memiliki struct dengan dua bidang, Addressnya akan menjadi h(h(sn_keccak(nama_variabel),k_1),k_2)
- di mana k_1
dan k_2
adalah nilai dari dua bidang struct.
Demikian pula, dalam kasus pemetaan bertingkat seperti LegacyMap((ContractAddress, ContractAddress), u8)
, Addressnya akan dihitung dengan cara yang sama: h(h(sn_keccak(nama_variabel),k_1),k_2)
.
Untuk lebih jelasnya tentang tata letak Storage kontrak, kunjungi Dokumentasi Starknet
Fungsi Kontrak
Pada bagian ini, kita akan melihat berbagai jenis fungsi yang dapat Anda temui dalam kontrak:
1. Konstruktor
Konstruktor adalah jenis fungsi khusus yang hanya dijalankan sekali saat mendeploy kontrak, dan dapat digunakan untuk menginisialisasi status sebuah kontrak.
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:constructor}}
Beberapa aturan penting yang perlu diperhatikan:
- Kontrak Anda tidak boleh memiliki lebih dari satu konstruktor.
- Fungsi konstruktor Anda harus diberi nama
constructor
. - Harus diberi anotasi dengan atribut
#[constructor]
.
2. Fungsi Publik
Seperti yang disebutkan sebelumnya, fungsi publik dapat diakses dari luar kontrak. Mereka harus didefinisikan di dalam blok implementasi yang dianotasi dengan atribut #[external(v0)]
. Atribut ini hanya memengaruhi keterlihatan (publik vs privat/internal), namun tidak memberi informasi kepada kita tentang kemampuan fungsi-fungsi ini untuk memodifikasi status kontrak.
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:impl_public}}
Fungsi Eksternal
Fungsi eksternal adalah fungsi yang dapat memodifikasi status sebuah kontrak. Mereka bersifat publik dan dapat dipanggil oleh kontrak lain atau dari luar.
Fungsi eksternal adalah fungsi publik di mana self: ContractState
dilewatkan sebagai referensi dengan kata kunci ref
, memungkinkan Anda untuk memodifikasi status kontrak.
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:external}}
Fungsi View
Fungsi view adalah fungsi hanya baca yang memungkinkan Anda mengakses data dari kontrak sambil memastikan bahwa status kontrak tidak dimodifikasi. Mereka dapat dipanggil oleh kontrak lain atau dari luar.
Fungsi view adalah fungsi publik di mana self: ContractState
dilewatkan sebagai snapshot, mencegah Anda untuk memodifikasi status kontrak.
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:view}}
Catatan: Penting untuk dicatat bahwa baik fungsi eksternal maupun fungsi view bersifat publik. Untuk membuat fungsi internal dalam sebuah kontrak, Anda perlu mendefinisikannya di luar blok implementasi yang dianotasi dengan atribut
#[external(v0)]
.
3. Fungsi Privat
Fungsi yang tidak didefinisikan dalam blok yang dianotasi dengan atribut #[external(v0)]
adalah fungsi privat (juga disebut fungsi internal). Mereka hanya dapat dipanggil dari dalam kontrak.
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:state_internal}}
Tunggu, apa itu atribut
#[generate_trait]
? Di mana definisi trait untuk blok implementasi ini? Nah, atribut#[generate_trait]
adalah atribut khusus yang memberi tahu kompiler untuk menghasilkan definisi trait untuk blok implementasi tersebut. Ini memungkinkan Anda untuk menghilangkan kode boilerplate dari definisi trait dan implementasinya untuk blok implementasi. Kita akan melihat lebih lanjut tentang ini di bagian selanjutnya.
Pada titik ini, Anda mungkin masih bertanya-tanya apakah semua ini benar-benar diperlukan jika Anda tidak perlu mengakses status kontrak dalam fungsi Anda (misalnya, fungsi bantuan/pustaka). Sebenarnya, Anda juga dapat mendefinisikan fungsi internal di luar blok implementasi. Satu-satunya alasan mengapa kita perlu mendefinisikan fungsi di dalam blok impl adalah jika kita ingin mengakses status kontrak.
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:stateless_internal}}
Event
Event adalah struktur data kustom yang dipancarkan oleh kontrak pintar selama eksekusi. Mereka memberikan cara bagi kontrak pintar untuk berkomunikasi dengan dunia luar dengan mencatat informasi tentang kejadian-kejadian tertentu dalam sebuah kontrak.
Event memainkan peran penting dalam pembuatan kontrak pintar. Ambil contoh Token Non-Fungible (NFT) yang dibuat di Starknet. Semua ini diindeks dan disimpan dalam sebuah database, kemudian ditampilkan kepada pengguna melalui penggunaan Event-Event ini. Mengabaikan untuk menyertakan sebuah Event dalam kontrak NFT Anda dapat menyebabkan pengalaman pengguna yang buruk. Hal ini karena pengguna mungkin tidak melihat NFT mereka muncul di Wallet mereka (Wallet menggunakan indeks ini untuk menampilkan NFT pengguna).
Mendefinisikan Event
Semua Event yang berbeda dalam kontrak didefinisikan di bawah Event
enum, yang mengimplementasikan trait starknet::Event
, sebagai variant enum. Trait ini didefinisikan dalam pustaka inti seperti berikut:
{{#include ../listings/ch99-starknet-smart-contracts/no_listing_event_trait/src/lib.cairo}}
Atribut #[derive(starknet::Event)]
menyebabkan kompiler untuk menghasilkan implementasi untuk trait di atas, diinstansiasi dengan tipe Event kita, yang dalam contoh kami adalah enum berikut:
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:event}}
Setiap variant Event harus menjadi sebuah struct dengan nama yang sama dengan variant tersebut, dan setiap variant harus mengimplementasikan trait starknet::Event
itu sendiri. Lebih lanjut, anggota-anggota dari variant-variant ini harus mengimplementasikan trait Serde
(lihat Lampiran C: Serializing with Serde), karena kunci/data ditambahkan ke Event menggunakan proses serialisasi.
Implementasi otomatis dari trait starknet::Event
akan mengimplementasikan fungsi append_keys_and_data
untuk setiap variant dari enum Event
kita. Implementasi yang dihasilkan akan menambahkan sebuah kunci tunggal berdasarkan nama variant (StoredName
), dan kemudian secara rekursif memanggil append_keys_and_data
dalam impl dari trait Event untuk tipe variant tersebut.
Dalam kontrak kami, kami mendefinisikan sebuah Event bernama StoredName
yang memancarkan Address kontrak pemanggil dan nama yang disimpan dalam kontrak, di mana bidang user
diserialisasikan sebagai kunci dan bidang name
diserialisasikan sebagai data.
Untuk mengindeks kunci dari sebuah Event, cukup beri anotasi dengan #[key]
seperti yang ditunjukkan dalam contoh untuk kunci user
.
Saat memancarkan Event dengan self.emit(StoredName { user: user, name: name })
, sebuah kunci yang sesuai dengan nama StoredName
, khususnya sn_keccak(StoredName)
, ditambahkan ke daftar kunci. user
diserialisasikan sebagai kunci, berkat atribut #[key]
, sementara Address diserialisasikan sebagai data. Setelah semuanya diproses, kita mendapatkan kunci dan data berikut: keys = [sn_keccak("StoredName"), user]
dan data = [address]
.
Memancarkan Event
Setelah mendefinisikan Event, kita dapat memancarkannya menggunakan self.emit
, dengan sintaks berikut:
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:emit_event}}
Mengurangi kode yang berulang
Pada bagian sebelumnya, kita melihat contoh blok implementasi dalam kontrak yang tidak memiliki trait yang sesuai.
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_03_example_contract/src/lib.cairo:state_internal}}
Ini bukan kali pertama kita menemui atribut ini, kita sudah membicarakannya dalam Traits in Cairo. Pada bagian ini, kita akan melihat lebih dalam tentang atribut tersebut dan melihat bagaimana penggunaannya dalam kontrak-kontrak.
Ingatlah bahwa untuk mengakses ContractState dalam sebuah fungsi, fungsi tersebut harus didefinisikan dalam blok implementasi yang parameter generiknya adalah ContractState
. Hal ini menyiratkan bahwa kita pertama-tama perlu mendefinisikan trait generik yang mengambil TContractState
, dan kemudian mengimplementasikan trait ini untuk tipe ContractState
.
Namun dengan menggunakan atribut #[generate_trait]
, seluruh proses ini dapat dilewati dan kita bisa langsung mendefinisikan blok implementasi, tanpa parameter generik apapun, dan menggunakan self: ContractState
dalam fungsi-fungsi kita.
Jika kita harus secara manual mendefinisikan trait untuk implementasi InternalFunctions
, akan terlihat seperti ini:
{{#include ../listings/ch99-starknet-smart-contracts/no_listing_99_02_explicit_internal_fn/src/lib.cairo:state_internal}}
Optimisasi Penyimpanan dengan StorePacking
Bit-packing adalah konsep sederhana: Gunakan sebanyak mungkin bit untuk menyimpan sebuah data. Ketika dilakukan dengan baik, ini dapat secara signifikan mengurangi ukuran data yang perlu Anda simpan. Hal ini terutama penting dalam kontrak pintar, di mana penyimpanan memiliki biaya yang mahal.
Ketika menulis kontrak pintar Cairo, penting untuk mengoptimalkan penggunaan penyimpanan untuk mengurangi biaya gas. Memang, sebagian besar biaya yang terkait dengan sebuah transaksi berhubungan dengan pembaruan penyimpanan; dan setiap slot penyimpanan membutuhkan biaya gas untuk ditulis. Ini berarti dengan mem-packing beberapa nilai ke dalam slot yang lebih sedikit, Anda dapat mengurangi biaya gas yang dikeluarkan oleh pengguna kontrak pintar Anda.
Cairo menyediakan trait StorePacking
untuk memungkinkan pem-packing bidang struct ke dalam slot penyimpanan yang lebih sedikit. Sebagai contoh, pertimbangkan sebuah struct Sizes
dengan 3 bidang dari tipe yang berbeda. Ukuran totalnya adalah 8 + 32 + 64 = 104 bit. Ini kurang dari 128 bit dari sebuah u128
tunggal. Artinya kita dapat mem-packing semua 3 bidang ke dalam sebuah variabel u128
tunggal. Karena sebuah slot penyimpanan dapat menampung hingga 251 bit, nilai yang ter-packing kita akan hanya membutuhkan satu slot penyimpanan daripada 3.
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_13_storage_packing/src/lib.cairo:here}}
Mengoptimalkan penyimpanan dengan mengimplementasikan trait StorePacking
Fungsi pack
menggabungkan ketiga bidang ke dalam sebuah nilai u128
tunggal dengan melakukan bitshift dan penambahan. Fungsi unpack
membalikkan proses ini untuk mengekstrak kembali bidang-bidang asli ke dalam sebuah struct.
Jika Anda tidak familiar dengan operasi bit, berikut penjelasan dari operasi-operasi yang dilakukan dalam contoh ini:
Tujuannya adalah mem-packing bidang tiny
, small
, dan medium
ke dalam sebuah nilai u128
tunggal.
Pertama, saat mem-packing:
tiny
adalahu8
sehingga kita hanya mengonversinya secara langsung keu128
dengan.into()
. Ini menciptakan sebuah nilaiu128
dengan 8 bit rendah diatur ke nilai daritiny
.small
adalahu32
sehingga pertama-tama kita menggesernya ke kiri sebanyak 8 bit (menambahkan 8 bit dengan nilai 0 ke kiri) untuk membuat ruang untuk 8 bit yang diambil olehtiny
. Lalu kita tambahkantiny
kesmall
untuk menggabungkannya menjadi sebuah nilaiu128
tunggal. Nilai daritiny
sekarang mengambil bit 0-7 dan nilai darismall
mengambil bit 8-39.- Demikian pula,
medium
adalahu64
sehingga kita menggesernya ke kiri sebanyak 40 bit (8 + 32) (TWO_POW_40
) untuk membuat ruang bagi bidang-bidang sebelumnya. Ini mengambil bit 40-103.
Ketika melakukan pem-unpacking:
- Pertama, kita ekstrak
tiny
dengan melakukan bitwise AND (&) dengan sebuah bitmask dari 8 satu (& MASK_8
). Ini mengisolasi 8 bit terendah dari nilai yang ter-pack, yang merupakan nilaitiny
. - Untuk
small
, kita geser ke kanan sebanyak 8 bit (/ TWO_POW_8
) untuk menyesuaikannya dengan bitmask, lalu gunakan bitwise AND dengan bitmask 32 satu. - Untuk
medium
, kita geser ke kanan sebanyak 40 bit. Karena ini adalah nilai terakhir yang di-pack, kita tidak perlu menerapkan bitmask karena bit yang lebih tinggi sudah 0.
Teknik ini dapat digunakan untuk setiap kelompok bidang yang cocok dalam ukuran bit dari tipe penyimpanan yang ter-pack. Sebagai contoh, jika Anda memiliki sebuah struct dengan beberapa bidang yang ukuran bit-nya totalnya adalah 256 bit, Anda dapat mem-pack mereka ke dalam variabel u256
tunggal. Jika ukuran bit-nya totalnya adalah 512 bit, Anda dapat mem-pack mereka ke dalam variabel u512
tunggal, dan seterusnya. Anda dapat mendefinisikan struct dan logika Anda sendiri untuk mem-pack dan mem-unpack mereka.
Sisa pekerjaan dilakukan secara ajaib oleh compiler - jika sebuah tipe mengimplementasikan trait StorePacking
, maka compiler akan tahu bahwa dapat menggunakan implementasi StoreUsingPacking
dari trait Store
untuk mem-pack sebelum menulis dan mem-unpack setelah membaca dari penyimpanan.
Satu detail penting, namun, adalah bahwa tipe yang dihasilkan oleh StorePacking::pack
juga harus mengimplementasikan Store
agar StoreUsingPacking
dapat berfungsi. Kebanyakan waktu, kita akan ingin mem-pack ke dalam felt252 atau u256 - namun jika Anda ingin mem-pack ke dalam tipe milik Anda sendiri, pastikan bahwa tipe tersebut mengimplementasikan trait Store
.
Komponen: Blok Bangunan Mirip Lego untuk Kontrak Pintar
Mengembangkan kontrak yang berbagi logika dan penyimpanan umum dapat menyulitkan dan rentan terhadap bug, karena logika tersebut sulit untuk digunakan kembali dan perlu diimplementasikan ulang dalam setiap kontrak. Tetapi bagaimana jika ada cara untuk menyematkan hanya fungsionalitas tambahan yang Anda butuhkan di dalam kontrak Anda, memisahkan logika inti kontrak Anda dari bagian lainnya?
Komponen menyediakan tepat itu. Mereka adalah add-on modular yang mengemas logika, penyimpanan, dan acara yang dapat digunakan kembali yang dapat disertakan ke dalam beberapa kontrak. Mereka dapat digunakan untuk memperluas fungsionalitas kontrak tanpa harus mengimplementasikan ulang logika yang sama berulang kali.
Pikirkan tentang komponen sebagai blok Lego. Mereka memungkinkan Anda untuk memperkaya kontrak Anda dengan menyematkan modul yang Anda tulis sendiri atau oleh orang lain. Modul ini bisa sederhana, seperti komponen kepemilikan, atau lebih kompleks seperti token ERC20 yang lengkap.
Sebuah komponen adalah modul terpisah yang dapat berisi penyimpanan, acara, dan fungsi. Berbeda dengan kontrak, komponen tidak dapat dideklarasikan atau didaftarkan. Logikanya pada akhirnya akan menjadi bagian dari bytecode kontrak tempat ia disematkan.
Apa Saja yang Ada di dalam Sebuah Komponen?
Sebuah komponen sangat mirip dengan kontrak. Ia dapat berisi:
- Variabel penyimpanan
- Acara (events)
- Fungsi eksternal dan internal
Berbeda dengan kontrak, komponen tidak dapat dideploy secara mandiri. Kode komponen akan menjadi bagian dari kontrak tempat komponen tersebut disematkan.
Membuat Komponen
Untuk membuat sebuah komponen, pertama-tama tentukan dalam modulnya sendiri yang diberi atribut #[starknet::component]
. Dalam modul ini, Anda dapat mendeklarasikan struktur Storage
dan enumerasi Event
, seperti biasanya dilakukan dalam Kontrak.
Langkah selanjutnya adalah mendefinisikan antarmuka komponen, yang berisi tanda tangan fungsi yang akan memungkinkan akses eksternal ke logika komponen. Anda dapat mendefinisikan antarmuka komponen dengan mendeklarasikan sebuah trait dengan atribut #[starknet::interface]
, sama seperti yang dilakukan dengan kontrak. Antarmuka ini akan digunakan untuk memungkinkan akses eksternal ke fungsi-fungsi komponen menggunakan pola Dispatcher.
Implementasi sebenarnya dari logika eksternal komponen dilakukan dalam blok impl
yang ditandai sebagai #[embeddable_as(nama)]
. Biasanya, blok impl
ini akan menjadi implementasi dari trait yang mendefinisikan antarmuka komponen.
Catatan:
nama
adalah nama yang akan kita gunakan dalam kontrak untuk merujuk ke komponen tersebut. Ini berbeda dengan nama impl Anda.
Anda juga dapat mendefinisikan fungsi internal yang tidak akan dapat diakses secara eksternal, dengan hanya mengabaikan atribut #[embeddable_as(nama)]
di atas blok impl
internal. Anda akan dapat menggunakan fungsi internal ini di dalam kontrak di mana Anda menyematkan komponen tersebut, tetapi tidak dapat berinteraksi dengannya dari luar, karena mereka bukan bagian dari abi kontrak.
Fungsi-fungsi dalam blok impl
ini mengharapkan argumen seperti ref self: ComponentState<TContractState>
(untuk fungsi yang mengubah status) atau self: @ComponentState<TContractState>
(untuk fungsi tampilan). Hal ini membuat impl menjadi generik terhadap TContractState
, memungkinkan kita untuk menggunakan komponen ini dalam berbagai kontrak.
Contoh: sebuah Komponen Ownable
⚠️ Contoh yang ditunjukkan di bawah belum diaudit dan tidak dimaksudkan untuk digunakan dalam produksi. Para penulis tidak bertanggung jawab atas kerusakan yang disebabkan oleh penggunaan kode ini.
Antarmuka dari komponen Ownable, yang mendefinisikan metode-metode yang tersedia secara eksternal untuk mengelola kepemilikan suatu kontrak, akan terlihat seperti ini:
{{#include ../listings/ch99-starknet-smart-contracts/components/listing_01_ownable/src/component.cairo:interface}}
Komponen itu sendiri didefinisikan sebagai:
{{#include ../listings/ch99-starknet-smart-contracts/components/listing_01_ownable/src/component.cairo:component}}
Sintaks ini sebenarnya cukup mirip dengan sintaks yang digunakan untuk kontrak. Satu-satunya perbedaan terkait atribut #[embeddable_as]
di atas impl dan genericity blok impl yang akan kita kupas secara detail.
Seperti yang dapat Anda lihat, komponen kami memiliki dua blok impl
: satu yang sesuai dengan implementasi trait antarmuka, dan satu berisi metode-metode yang seharusnya tidak diekspos secara eksternal dan hanya dimaksudkan untuk penggunaan internal. Mengekspos assert_only_owner
sebagai bagian dari antarmuka tidak akan masuk akal, karena hanya dimaksudkan untuk digunakan secara internal oleh kontrak yang menyematkan komponen.
Melihat Lebih Dekat pada Blok impl
{{#include ../listings/ch99-starknet-smart-contracts/components/listing_01_ownable/src/component.cairo:impl_signature}}
Atribut #[embeddable_as]
digunakan untuk menandai impl sebagai yang dapat disematkan di dalam kontrak. Ini memungkinkan kita untuk menentukan nama impl yang akan digunakan dalam kontrak untuk merujuk ke komponen ini. Dalam kasus ini, komponen akan disebut sebagai Ownable
dalam kontrak yang menyematkannya.
Implementasinya sendiri generik terhadap ComponentState<TContractState>
, dengan batasan tambahan bahwa TContractState
harus mengimplementasikan trait HasComponent<T>
. Ini memungkinkan kita untuk menggunakan komponen dalam berbagai kontrak, selama kontrak mengimplementasikan trait HasComponent
. Memahami mekanisme ini secara detail tidak diperlukan untuk menggunakan komponen, tetapi jika Anda penasaran tentang cara kerjanya, Anda dapat membaca lebih lanjut di bagian Komponen di balik layar.
Salah satu perbedaan utama dari kontrak pintar biasa adalah bahwa akses ke penyimpanan dan acara dilakukan melalui tipe generik ComponentState<TContractState>
dan bukan ContractState
. Perhatikan bahwa meskipun tipe berbeda, akses penyimpanan atau pemicuan acara dilakukan dengan cara yang serupa melalui self.storage_var_name.read()
atau self.emit(...)
.
Catatan: Untuk menghindari kebingungan antara nama yang dapat disematkan dan nama impl, kami merekomendasikan untuk tetap menggunakan sufiks
Impl
dalam nama impl.
Memigrasi Kontrak ke Sebuah Komponen
Karena baik kontrak maupun komponen memiliki banyak kesamaan, sebenarnya sangat mudah untuk bermigrasi dari kontrak ke komponen. Perubahan yang diperlukan hanya:
- Menambahkan atribut
#[starknet::component]
ke dalam modul. - Menambahkan atribut
#[embeddable_as(nama)]
ke blokimpl
yang akan disematkan dalam kontrak lain. - Menambahkan parameter generic ke blok
impl
:- Menambahkan
TContractState
sebagai parameter generic. - Menambahkan
+HasComponent<TContractState>
sebagai batasan impl.
- Menambahkan
- Mengubah tipe argumen
self
dalam fungsi-fungsi di dalam blokimpl
menjadiComponentState<TContractState>
bukanContractState
.
Untuk trait yang tidak memiliki definisi eksplisit dan dihasilkan menggunakan #[generate_trait]
, logikanya sama - namun trait tersebut generik terhadap TContractState
bukan ComponentState<TContractState>
, seperti yang ditunjukkan dalam contoh dengan InternalTrait
.
Menggunakan komponen di dalam sebuah kontrak
Kekuatan utama dari komponen adalah bagaimana hal itu memungkinkan penggunaan kembali primitif yang telah dibangun di dalam kontrak Anda dengan sejumlah kecil boilerplate yang terbatas. Untuk mengintegrasikan sebuah komponen ke dalam kontrak Anda, Anda perlu:
-
Mendeklarasikannya dengan macro
component!()
, dengan menyebutkan- Path ke komponen
path::to::component
. - Nama variabel penyimpanan dalam penyimpanan kontrak Anda yang merujuk ke penyimpanan komponen ini (misalnya
ownable
). - Nama variabel dalam enum acara kontrak Anda yang merujuk ke acara komponen ini (misalnya
OwnableEvent
).
- Path ke komponen
-
Menambahkan path ke penyimpanan dan acara komponen ke
Storage
danEvent
kontrak. Mereka harus cocok dengan nama yang diberikan pada langkah 1 (misalnyaownable: ownable_component::Storage
danOwnableEvent: ownable_component::Event
).Variabel penyimpanan HARUS diberi atribut
#[substorage(v0)]
. -
Menyematkan logika komponen yang didefinisikan di dalam kontrak Anda, dengan menginstansiasi impl generik komponen dengan
ContractState
konkret menggunakan sebuah alias impl. Alias ini harus diberi anotasi#[abi(embed_v0)]
untuk secara eksternal mengekspos fungsi-fungsi komponen.Seperti yang dapat Anda lihat, InternalImpl tidak ditandai dengan
#[abi(embed_v0)]
. Memang, kami tidak ingin mengekspos secara eksternal fungsi-fungsi yang didefinisikan dalam impl ini. Namun, mungkin kita masih ingin mengaksesnya secara internal.
Contohnya, untuk menyematkan komponen Ownable
yang didefinisikan di atas, kita akan melakukan hal berikut:
{{#include ../listings/ch99-starknet-smart-contracts/components/listing_01_ownable/src/contract.cairo:all}}
Logika komponen sekarang menjadi bagian dari kontrak tanpa hambatan! Kita dapat berinteraksi dengan fungsi-fungsi komponen secara eksternal dengan memanggilnya menggunakan IOwnableDispatcher
yang diinisialisasi dengan Address kontrak.
{{#include ../listings/ch99-starknet-smart-contracts/components/listing_01_ownable/src/component.cairo:interface}}
Menggabungkan Komponen untuk Komposabilitas Maksimal
Komposabilitas dari komponen benar-benar terlihat ketika menggabungkan beberapa komponen bersama-sama. Setiap komponen menambahkan fiturnya ke kontrak. Anda akan dapat bergantung pada implementasi komponen-komponen Openzeppelin's di masa mendatang untuk dengan cepat menyematkan semua fungsionalitas umum yang Anda butuhkan dalam sebuah kontrak.
Para pengembang dapat fokus pada logika inti kontrak mereka sambil mengandalkan komponen-komponen yang telah diuji dan diaudit untuk segala sesuatu yang lain.
Komponen bahkan dapat bergantung pada komponen lain dengan membatasi
TContractstate
yang mereka generikkan untuk mengimplementasikan trait dari komponen lain. Sebelum kita masuk ke dalam mekanisme ini, mari kita pertama-tama melihat bagaimana komponen bekerja di bawah
kap.
Troubleshooting
Anda mungkin mengalami beberapa kesalahan saat mencoba mengimplementasikan komponen. Sayangnya, beberapa dari mereka tidak memiliki pesan kesalahan yang bermakna untuk membantu dalam debugging. Bagian ini bertujuan untuk memberikan beberapa petunjuk untuk membantu Anda memperbaiki kode Anda.
-
Trait not found. Not a trait.
Kesalahan ini dapat terjadi ketika Anda tidak mengimpor blok impl komponen dengan benar ke dalam kontrak Anda. Pastikan untuk mematuhi sintaks berikut:
#[abi(embed_v0)] impl NAMA_IMPL = komponen::NAMA_TERSIMPAN<ContractState>
Merujuk ke contoh sebelumnya kita, ini akan menjadi:
#[abi(embed_v0)] impl OwnableImpl = upgradeable::Ownable<ContractState>
-
Plugin diagnostic: name is not a substorage member in the contract's Storage. Consider adding to Storage: (...)
Compiler membantu Anda banyak dalam debugging ini dengan memberikan rekomendasi tindakan yang harus diambil. Pada dasarnya, Anda lupa menambahkan penyimpanan komponen ke penyimpanan kontrak Anda. Pastikan untuk menambahkan path ke penyimpanan komponen yang diberi atribut
#[substorage(v0)]
ke penyimpanan kontrak Anda. -
Plugin diagnostic: name is not a nested event in the contract's Event enum. Consider adding to the Event enum:
Serupa dengan kesalahan sebelumnya, compiler memberitahu Anda bahwa Anda lupa menambahkan acara komponen ke acara kontrak Anda. Pastikan untuk menambahkan path ke acara komponen ke acara kontrak Anda.
-
Fungsi-fungsi komponen tidak dapat diakses secara eksternal
Hal ini dapat terjadi jika Anda lupa memberi blok impl komponen dengan anotasi
#[abi(embed_v0)]
. Pastikan untuk menambahkan anotasi ini saat menyematkan blok impl komponen dalam kontrak Anda.
Komponen di dalam mesin
Komponen memberikan modularitas yang kuat pada kontrak Starknet. Tetapi bagaimana sebenarnya sihir ini terjadi di balik layar?
Bab ini akan menjelajahi secara mendalam tentang internal kompilator untuk menjelaskan mekanisme yang memungkinkan komposabilitas komponen.
Pengantar tentang Implementasi yang Dapat Ditanam
Sebelum mempelajari lebih dalam tentang komponen, kita perlu memahami implementasi yang dapat ditanam.
Sebuah implementasi dari trait antarmuka Starknet (ditandai dengan #[starknet::interface]
) dapat dibuat dapat ditanam. Implementasi yang dapat ditanam dapat disisipkan ke dalam kontrak apa pun, menambahkan titik masuk baru dan memodifikasi ABI dari kontrak tersebut.
Mari kita lihat contoh untuk melihat hal ini dalam aksi:
{{#include ../listings/ch99-starknet-smart-contracts/components/no_listing_01_embeddable/src/lib.cairo}}
Dengan menyisipkan SimpleImpl
, kita secara eksternal mengekspos ret4
dalam ABI kontrak.
Sekarang setelah kita lebih familiar dengan mekanisme penyisipan, kita dapat melihat bagaimana komponen membangun dari hal ini.
Di Dalam Komponen: Implementasi Generik
Ingatlah sintaks blok impl yang digunakan dalam komponen:
{{#include ../listings/ch99-starknet-smart-contracts/components/listing_01_ownable/src/component.cairo:impl_signature}}
Poin-poin kunci:
-
OwnableImpl
membutuhkan implementasi dari traitHasComponent<TContractState>
oleh kontrak yang mendasarinya, yang secara otomatis dihasilkan dengan macrocomponent!()
saat menggunakan sebuah komponen di dalam sebuah kontrak.Kompilator akan menghasilkan sebuah impl yang membungkus setiap fungsi di dalam
OwnableImpl
, mengganti argumenself: ComponentState<TContractState>
denganself: TContractState
, di mana akses ke status komponen dilakukan melalui fungsiget_component
dalam traitHasComponent<TContractState>
.Untuk setiap komponen, kompilator akan menghasilkan trait
HasComponent
. Trait ini mendefinisikan antarmuka untuk menjembatani antaraTContractState
sebenarnya dari kontrak generik, danComponentState<TContractState>
.// dihasilkan per komponen trait HasComponent<TContractState> { fn get_component(self: @TContractState) -> @ComponentState<TContractState>; fn get_component_mut(ref self: TContractState) -> ComponentState<TContractState>; fn get_contract(self: @ComponentState<TContractState>) -> @TContractState; fn get_contract_mut(ref self: ComponentState<TContractState>) -> TContractState; fn emit<S, impl IntoImp: traits::Into<S, Event>>(ref self: ComponentState<TContractState>, event: S); }
Dalam konteks kita,
ComponentState<TContractState>
adalah tipe yang khusus untuk komponen ownable, yaitu memiliki anggota berdasarkan variabel penyimpanan yang didefinisikan dalamownable_component::Storage
. Berpindah dariTContractState
generik keComponentState<TContractState>
akan memungkinkan kita untuk menyisipkanOwnable
ke dalam kontrak apa pun yang ingin menggunakannya. Arah sebaliknya (ComponentState<TContractState>
keContractState
) berguna untuk dependensi (lihat contoh implementasiUpgradeable
komponen yang bergantung pada implementasiIOwnable
di bagian Ketergantungan Komponen).Singkatnya, seseorang harus memikirkan implementasi dari
HasComponent<T>
di atas sebagai mengatakan: "Kontrak yang memiliki status T memiliki komponen yang dapat diupgrade". -
Ownable
dianotasi dengan atributembeddable_as(<nama>)
:embeddable_as
mirip denganembeddable
; ia hanya berlaku untukimpls
dari traitstarknet::interface
dan memungkinkan penyisipan implementasi ini di dalam modul kontrak. Dengan demikian,embeddable_as(<nama>)
memiliki peran lain dalam konteks komponen. Pada akhirnya, ketika menyisipkanOwnableImpl
ke dalam suatu kontrak, kita berharap mendapatkan sebuah impl dengan fungsi-fungsi berikut:{{#include ../listings/ch99-starknet-smart-contracts/components/listing_01_ownable/src/component.cairo:trait_def}}
Perhatikan bahwa meskipun dimulai dengan sebuah fungsi yang menerima tipe generik
ComponentState<TContractState>
, kita ingin berakhir dengan sebuah fungsi yang menerimaContractState
. Di sinilahembeddable_as(<nama>)
berperan. Untuk melihat gambaran keseluruhan, kita perlu melihat impl yang dihasilkan oleh kompilator karena adanya anotasiembeddable_as(Ownable)
:#[starknet::embeddable] impl Ownable< TContractState, +HasComponent<TContractState> , impl TContractStateDrop: Drop<TContractState> > of super::IOwnable<TContractState> { fn owner(self: @TContractState) -> ContractAddress { let component = HasComponent::get_component(self); OwnableImpl::owner(component, ) } fn transfer_ownership(ref self: TContractState, new_owner: ContractAddress ) { let mut component = HasComponent::get_component_mut(ref self); OwnableImpl::transfer_ownership(ref component, new_owner, ) } fn renounce_ownership(ref self: TContractState) { let mut component = HasComponent::get_component_mut(ref self); OwnableImpl::renounce_ownership(ref component, ) } }
Perhatikan bahwa berkat adanya impl dari
HasComponent<TContractState>
, kompilator dapat membungkus fungsi-fungsi kita dalam sebuah impl baru yang tidak secara langsung mengetahui tentang tipeComponentState
.Ownable
, yang nama kita pilih saat menulisembeddable_as(Ownable)
, adalah impl yang akan kita sisipkan di dalam sebuah kontrak yang menginginkan kepemilikan.
Integrasi Kontrak
Kita telah melihat bagaimana implementasi generik memungkinkan kegunaan ulang komponen. Selanjutnya, mari kita lihat bagaimana sebuah kontrak mengintegrasikan sebuah komponen.
Kontrak menggunakan alias impl untuk menginisialisasi implementasi generik komponen dengan tipe konkret ContractState
dari kontrak.
{{#include ../listings/ch99-starknet-smart-contracts/components/listing_01_ownable/src/contract.cairo:embedded_impl}}
Baris di atas menggunakan mekanisme penyisipan impl Cairo bersamaan dengan sintaks alias impl. Kita menginisialisasi OwnableImpl<TContractState>
yang generik dengan tipe konkret ContractState
. Ingatlah bahwa OwnableImpl<TContractState>
memiliki parameter impl generik HasComponent<TContractState>
. Implementasi dari trait ini dihasilkan oleh macro component!
.
Perhatikan bahwa hanya kontrak yang menggunakan yang dapat mengimplementasikan trait ini karena hanya kontrak yang mengetahui tentang kedua status kontrak dan status komponen.
Ini menyatukan semua hal untuk menyisipkan logika komponen ke dalam kontrak.
Simpulan Penting
- Impl yang dapat disisipkan memungkinkan penyisipan logika komponen ke dalam kontrak dengan menambahkan titik masuk dan memodifikasi ABI kontrak.
- Kompilator secara otomatis menghasilkan implementasi trait
HasComponent
saat sebuah komponen digunakan dalam sebuah kontrak. Ini menciptakan jembatan antara status kontrak dan status komponen, memungkinkan interaksi di antara keduanya. - Komponen mengemas logika yang dapat digunakan kembali secara generik, tanpa tergantung pada kontrak tertentu.
Kontrak mengintegrasikan komponen melalui alias impl dan mengaksesnya melalui trait
HasComponent
yang dihasilkan. - Komponen membangun dari impl yang dapat disisipkan dengan mendefinisikan logika komponen yang generik yang dapat diintegrasikan ke dalam setiap kontrak yang ingin menggunakan komponen tersebut. Alias impl menginisialisasi impl generik ini dengan tipe penyimpanan konkret kontrak.
Komponen dependencies
Pengujian Komponen
Pengujian komponen sedikit berbeda dibandingkan dengan pengujian kontrak.
Kontrak perlu diuji terhadap keadaan tertentu, yang dapat dicapai dengan cara mendeploy kontrak dalam sebuah uji coba, atau dengan hanya mendapatkan objek ContractState
dan memodifikasinya dalam konteks uji coba Anda.
Komponen adalah konstruk umum, yang dimaksudkan untuk diintegrasikan dalam kontrak, yang tidak dapat didedikasikan sendiri dan tidak memiliki objek ContractState
yang dapat kita gunakan. Jadi, bagaimana kita mengujinya?
Mari kita pertimbangkan bahwa kita ingin menguji komponen yang sangat sederhana bernama "Counter", yang akan memungkinkan setiap kontrak memiliki penghitung yang dapat diinkrementasi. Komponen ini didefinisikan sebagai berikut:
{{#include ../listings/ch99-starknet-smart-contracts/components/listing_03_test_component/src/counter.cairo:component}}
Pengujian komponen dengan mendeploy kontrak tiruan
Cara termudah untuk menguji komponen adalah dengan mengintegrasikannya dalam kontrak tiruan. Kontrak tiruan ini hanya digunakan untuk tujuan pengujian, dan hanya mengintegrasikan komponen yang ingin Anda uji. Ini memungkinkan Anda untuk menguji komponen dalam konteks sebuah kontrak, dan menggunakan Dispatcher untuk memanggil titik masuk komponen.
Kita dapat mendefinisikan kontrak tiruan tersebut sebagai berikut:
{{#include ../listings/ch99-starknet-smart-contracts/components/listing_03_test_component/src/lib.cairo:mock_contract}}
Kontrak ini sepenuhnya didedikasikan untuk pengujian komponen Counter
. Ini menyematkan komponen dengan macro component!
, mengekspos titik masuk komponen dengan memberikan anotasi alias impl dengan #[abi(embed_v0)]
.
Kita juga perlu mendefinisikan sebuah antarmuka yang akan diperlukan untuk berinteraksi secara eksternal dengan kontrak tiruan ini.
{{#include ../listings/ch99-starknet-smart-contracts/components/listing_03_test_component/src/counter.cairo:interface}}
Sekarang kita dapat menulis pengujian untuk komponen dengan mendeploy kontrak tiruan ini dan memanggil titik masuknya, seperti yang kita lakukan dengan kontrak biasa.
{{#include ../listings/ch99-starknet-smart-contracts/components/listing_03_test_component/src/tests_deployed.cairo}}
Pengujian Komponen tanpa Mendeploy Kontrak
Pada Komponen di bawah Tampilan, kita melihat bahwa komponen memanfaatkan genericity untuk mendefinisikan penyimpanan dan logika yang dapat disematkan dalam beberapa kontrak. Jika sebuah kontrak menyematkan sebuah komponen, sebuah HasComponent
trait dibuat dalam kontrak ini, dan metode-metode komponen dibuat tersedia.
Hal ini memberi informasi kepada kita bahwa jika kita dapat menyediakan sebuah TContractState
konkret yang mengimplementasikan trait HasComponent
ke dalam struktur ComponentState
, seharusnya kita dapat langsung memanggil metode-metode dari komponen menggunakan objek ComponentState
konkret ini, tanpa harus mendeploy mock.
Mari kita lihat bagaimana kita dapat melakukannya dengan menggunakan type alias. Kita masih perlu mendefinisikan sebuah kontrak tiruan - mari gunakan yang sama seperti di atas - tetapi kali ini, kita tidak perlu mendeploynya.
Pertama, kita perlu mendefinisikan sebuah implementasi konkret dari tipe generic ComponentState
menggunakan sebuah type alias. Kita akan menggunakan tipe MockContract::ContractState
untuk melakukannya.
{{#rustdoc_include ../listings/ch99-starknet-smart-contracts/components/listing_03_test_component/src/tests_direct.cairo:type_alias}}
Kami mendefinisikan tipe TestingState
sebagai alias dari tipe CounterComponent::ComponentState<MockContract::ContractState>
. Dengan melewati tipe MockContract::ContractState
sebagai tipe konkret untuk ComponentState
, kami memberikan alias implementasi konkret dari struktur ComponentState
menjadi TestingState
.
Karena MockContract
menyematkan CounterComponent
, metode-metode dari CounterComponent
yang didefinisikan dalam blok CounterImpl
sekarang dapat digunakan pada objek TestingState
.
Sekarang setelah kita telah membuat metode-metode ini tersedia, kita perlu menginisialisasi sebuah objek tipe TestingState
yang akan kita gunakan untuk menguji komponen. Kita dapat melakukannya dengan memanggil fungsi component_state_for_testing
, yang secara otomatis menyimpulkan bahwa itu harus mengembalikan objek tipe TestingState
.
Kita bahkan dapat mengimplementasikannya sebagai bagian dari trait Default
, yang memungkinkan kita untuk mengembalikan TestingState
kosong dengan sintaks Default::default()
.
Mari kita ringkas apa yang telah kita lakukan sejauh ini:
- Kita mendefinisikan sebuah kontrak tiruan yang menyematkan komponen yang ingin kita uji.
- Kita mendefinisikan sebuah implementasi konkret dari
ComponentState<TContractState>
menggunakan type alias denganMockContract::ContractState
, yang kita beri namaTestingState
. - Kita mendefinisikan sebuah fungsi yang menggunakan
component_state_for_testing
untuk mengembalikan objekTestingState
.
Sekarang kita dapat menulis pengujian untuk komponen dengan memanggil fungsi-fungsi nya secara langsung, tanpa harus mendeploy kontrak tiruan. Pendekatan ini lebih ringan dibandingkan sebelumnya, dan memungkinkan pengujian fungsi internal dari komponen yang tidak mudah diekspos ke dunia luar.
{{#rustdoc_include ../listings/ch99-starknet-smart-contracts/components/listing_03_test_component/src/tests_direct.cairo:test}}
Kontrak Starknet: ABIs dan Interaksi Antar-Kontrak
Interaksi antara kontrak pintar adalah fitur penting saat membuat aplikasi terdesentralisasi yang kompleks, karena ini memungkinkan untuk komposabilitas dan pemisahan kepentingan. Bab ini memberikan wawasan tentang bagaimana membuat kontrak berinteraksi satu sama lain.
Secara khusus, Anda akan mempelajari tentang ABIs, antarmuka kontrak, dispatcher kontrak dan pustaka, serta setara panggilan sistem tingkat rendah mereka!
ABIs dan Antarmuka Kontrak
Interaksi lintas kontrak antara kontrak pintar pada blockchain adalah praktik umum yang memungkinkan kita membangun kontrak yang fleksibel sehingga dapat berkomunikasi satu sama lain.
Untuk mencapai hal ini di Starknet, diperlukan sesuatu yang disebut antarmuka.
ABI - Antarmuka Biner Aplikasi
Di Starknet, ABI dari suatu kontrak adalah representasi JSON dari fungsi-fungsi dan struktur kontrak, memberikan kemampuan kepada siapa pun (atau kontrak lainnya) untuk membentuk panggilan yang terenkripsi kepadanya. Ini adalah sebuah cetak biru yang memberi petunjuk bagaimana fungsi-fungsi seharusnya dipanggil, parameter masukan yang diharapkan, dan dalam format apa.
Meskipun kita menulis logika kontrak pintar kita dalam bahasa Cairo tingkat tinggi, mereka disimpan pada VM sebagai bytecode yang dapat dieksekusi dalam format biner. Karena bytecode ini tidak dapat dibaca oleh manusia, diperlukan interpretasi untuk dipahami. Di sinilah ABIs berperan, mendefinisikan metode-metode khusus yang dapat dipanggil ke suatu kontrak pintar untuk dieksekusi. Tanpa ABI, menjadi praktis tidak mungkin bagi aktor eksternal untuk memahami bagaimana berinteraksi dengan sebuah kontrak.
ABIs biasanya digunakan dalam antarmuka pengguna aplikasi terdesentralisasi (dApps), memungkinkan untuk memformat data dengan benar, sehingga dapat dimengerti oleh kontrak pintar dan sebaliknya. Ketika Anda berinteraksi dengan kontrak pintar melalui penjelajah blok seperti Voyager atau Starkscan, mereka menggunakan ABI kontrak untuk memformat data yang Anda kirim ke kontrak dan data yang dikembalikannya.
Interface
Antarmuka dari sebuah kontrak adalah daftar fungsi yang diekspos secara publik oleh kontrak tersebut. Antarmuka ini menentukan tanda tangan fungsi (nama, parameter, visibilitas, dan nilai kembalian) yang terdapat dalam kontrak pintar tanpa menyertakan badan fungsi.
Antarmuka kontrak dalam Cairo adalah traits yang diberi anotasi dengan atribut #[starknet::interface]
. Jika Anda baru mengenal traits, lihat bab khusus tentang traits.
Spesifikasi penting adalah bahwa trait ini harus generik terhadap tipe TContractState
. Hal ini diperlukan agar fungsi-fungsi dapat mengakses penyimpanan kontrak, sehingga mereka dapat membaca dan menulis ke dalamnya.
Catatan: Konstruktor kontrak tidak termasuk dalam antarmuka. Begitu pula fungsi internal tidak termasuk dalam antarmuka.
Berikut adalah contoh antarmuka untuk kontrak token ERC20. Seperti yang dapat Anda lihat, ini adalah trait generik terhadap tipe TContractState
. Fungsi-fungsi view
memiliki parameter self dengan tipe @TContractState
, sedangkan fungsi-fungsi external
memiliki parameter self dengan tipe yang dilewatkan dengan referensi ref self: TContractState
.
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_04_interface/src/lib.cairo}}
Daftar 99-4: Antarmuka ERC20 sederhana
Pada bab selanjutnya, kita akan melihat bagaimana kita dapat memanggil kontrak dari kontrak pintar lain menggunakan dispatchers dan syscalls.
Interaksi dengan kontrak dan kelas lain menggunakan Dispatchers dan syscalls
Setiap kali sebuah antarmuka kontrak didefinisikan, dua dispatcher secara otomatis dibuat dan diekspor oleh kompiler. Mari pertimbangkan sebuah antarmuka yang kami sebut IERC20, ini adalah:
- Contract Dispatcher
IERC20Dispatcher
- Library Dispatcher
IERC20LibraryDispatcher
Kompiler juga menghasilkan sebuah trait IERC20DispatcherTrait
, yang memungkinkan kita untuk memanggil fungsi-fungsi yang didefinisikan dalam antarmuka pada struktur dispatcher.
Pada bab ini, kita akan membahas apa itu dispatcher tersebut, bagaimana cara kerjanya, dan bagaimana menggunakannya.
Untuk memahami konsep-konsep dalam bab ini secara efektif, kita akan menggunakan antarmuka IERC20 dari bab sebelumnya (lihat Listing 99-4):
Contract Dispatcher
Seperti yang telah disebutkan sebelumnya, trait yang dianotasi dengan atribut #[starknet::interface]
secara otomatis menghasilkan sebuah dispatcher dan sebuah trait saat dikompilasi.
Antarmuka IERC20
kita diperluas menjadi sesuatu seperti ini:
Catatan: Kode yang diperluas untuk antarmuka IERC20 kita jauh lebih panjang, tetapi untuk menjaga bab ini ringkas dan langsung pada intinya, kita fokus pada satu fungsi view name
, dan satu fungsi eksternal transfer
.
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_05_dispatcher_trait/src/lib.cairo}}
Listing 99-5: Bentuk yang diperluas dari trait IERC20
Seperti yang bisa Anda lihat, dispatcher "klasik" hanyalah sebuah struktur yang membungkus Address kontrak dan mengimplementasikan DispatcherTrait
yang dihasilkan oleh kompiler, memungkinkan kita untuk memanggil fungsi-fungsi dari kontrak lain. Ini berarti kita dapat membuat sebuah struktur dengan Address kontrak yang ingin kita panggil, dan kemudian dengan mudah memanggil fungsi-fungsi yang didefinisikan dalam antarmuka pada struktur dispatcher seolah-olah mereka adalah metode dari tipe tersebut.
Perlu diperhatikan juga bahwa semua ini diabstraksikan di belakang layar berkat kekuatan dari plugin Cairo.
Memanggil Kontrak menggunakan Contract Dispatcher
Berikut adalah contoh sebuah kontrak yang bernama TokenWrapper
menggunakan dispatcher untuk memanggil fungsi yang didefinisikan pada token ERC-20. Memanggil transfer_token
akan mengubah status kontrak yang di-deploy pada contract_address
.
{{#rustdoc_include ../listings/ch99-starknet-smart-contracts/listing_99_06_sample_contract/src/lib.cairo:here}}
Listing 99-6: Sebuah kontrak contoh yang menggunakan Contract Dispatcher
Seperti yang bisa Anda lihat, kita harus mengimpor terlebih dahulu IERC20DispatcherTrait
dan IERC20Dispatcher
yang dihasilkan oleh kompiler, yang memungkinkan kita untuk melakukan panggilan ke metode-metode yang diimplementasikan untuk struktur IERC20Dispatcher
(name
, transfer
, dll.), dengan memberikan contract_address
dari kontrak yang ingin kita panggil pada struktur IERC20Dispatcher
.
Library Dispatcher
Perbedaan kunci antara dispatcher kontrak dan dispatcher library terletak pada konteks eksekusi logika yang didefinisikan dalam kelas tersebut. Sementara dispatcher reguler digunakan untuk memanggil fungsi dari kontrak (dengan status terkait), dispatcher library digunakan untuk memanggil kelas (tanpa status).
Mari kita pertimbangkan dua kontrak, A dan B.
Ketika A menggunakan IBDispatcher
untuk memanggil fungsi dari kontrak B, konteks eksekusi logika yang didefinisikan di B adalah milik B. Ini berarti nilai yang dikembalikan oleh get_caller_address()
di B akan mengembalikan Address A, dan memperbarui variabel penyimpanan di B akan memperbarui penyimpanan B.
Ketika A menggunakan IBLibraryDispatcher
untuk memanggil fungsi dari kelas B, konteks eksekusi logika yang didefinisikan di kelas B adalah milik A. Ini berarti nilai yang dikembalikan oleh variabel get_caller_address()
di kelas B akan mengembalikan Address pemanggil A, dan memperbarui variabel penyimpanan di kelas B akan memperbarui penyimpanan A (ingatlah bahwa kelas B tidak memiliki status; tidak ada status yang dapat diperbarui!)
Bentuk yang diperluas dari struct dan trait yang dihasilkan oleh kompiler terlihat seperti ini:
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_07_library_dispatcher/src/lib.cairo}}
Perhatikan bahwa perbedaan utama antara dispatcher kontrak reguler dan dispatcher library adalah bahwa yang pertama menggunakan call_contract_syscall
sedangkan yang terakhir menggunakan library_call_syscall
.
Listing 99-7: Bentuk yang diperluas dari trait IERC20
Memanggil Kontrak menggunakan Library Dispatcher
Berikut adalah contoh kode untuk memanggil kontrak menggunakan Library Dispatcher.
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_08_using_library_dispatcher/src/lib.cairo:here}}
Listing 99-8: Sebuah kontrak contoh yang menggunakan Library Dispatcher
Seperti yang dapat Anda lihat, kita harus mengimpor terlebih dahulu IContractBDispatcherTrait
dan IContractBLibraryDispatcher
ke dalam kontrak kita yang dihasilkan dari antarmuka kami oleh kompiler. Kemudian, kita dapat membuat sebuah instance dari IContractBLibraryDispatcher
dengan memberikan class_hash
dari kelas yang ingin kita panggil dengan library. Dari sana, kita dapat memanggil fungsi-fungsi yang didefinisikan dalam kelas tersebut, menjalankan logiknya dalam konteks kontrak kita. Ketika kita memanggil set_value
pada KontrakA, itu akan membuat panggilan library ke fungsi set_value
di KontrakB, memperbarui nilai variabel penyimpanan value
di KontrakA.
Menggunakan syscalls tingkat rendah
Cara lain untuk memanggil kontrak dan kelas lain adalah dengan menggunakan starknet::call_contract_syscall
dan starknet::library_call_syscall
. Dispatcher yang kita jelaskan di bagian sebelumnya adalah sintaksis tingkat tinggi untuk sistem panggilan tingkat rendah ini.
Menggunakan syscalls ini bisa berguna untuk penanganan kesalahan yang disesuaikan atau untuk mendapatkan lebih banyak kontrol atas serialisasi/deserialisasi data panggilan dan data yang dikembalikan. Berikut adalah contoh yang menunjukkan bagaimana menggunakan call_contract_syscall
untuk memanggil fungsi transfer
dari sebuah kontrak ERC20:
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_09_call_contract_syscall/src/lib.cairo}}
Listing 99-9: Sebuah kontrak contoh yang menggunakan syscalls
Untuk menggunakan syscall ini, kita memberikan Address kontrak, selector dari fungsi yang ingin kita panggil, dan argumen panggilan.
Argumen panggilan harus disediakan sebagai sebuah array dari felt252
. Untuk membangun array ini, kita melakukan serialisasi parameter fungsi yang diharapkan menjadi Array<felt252>
menggunakan trait Serde
, dan kemudian melewatkan array ini sebagai calldata. Pada akhirnya, kita akan mendapatkan kembali sebuah nilai yang telah diserialisasi yang perlu kita deserialisasi sendiri!
Contoh Lainnya
Bagian ini berisi contoh tambahan dari kontrak pintar Starknet, yang memanfaatkan berbagai fitur dari bahasa pemrograman Cairo. Kontribusi Anda sangat kami harapkan dan dihargai, karena kami bertujuan untuk mengumpulkan sebanyak mungkin contoh yang beragam.
Penyebaran dan Interaksi dengan Kontrak Voting
Kontrak Vote
dalam Starknet dimulai dengan mendaftarkan pemilih melalui konstruktor kontrak. Tiga pemilih diinisialisasi pada tahap ini, dan Address mereka diteruskan ke dalam fungsi internal _register_voters
. Fungsi ini menambahkan pemilih ke dalam status kontrak, menandai mereka sebagai terdaftar dan memenuhi syarat untuk memberikan suara.
Dalam kontrak ini, konstanta YES
dan NO
didefinisikan untuk mewakili opsi-opsi Voting (1 dan 0, secara berturut-turut). Konstanta-konstanta ini memfasilitasi proses Voting dengan standarisasi nilai-nilai input.
Setelah terdaftar, seorang pemilih dapat memberikan suara menggunakan fungsi vote
, memilih antara 1 (YES) atau 0 (NO) sebagai suara mereka. Ketika memberikan suara, status kontrak diperbarui, mencatat suara dan menandai pemilih telah memberikan suara. Ini memastikan bahwa pemilih tidak dapat memberikan suara lagi dalam proposal yang sama. Pemberian suara memicu peristiwa VoteCast
, mencatat tindakan tersebut.
Kontrak juga memantau upaya Voting yang tidak sah. Jika tindakan yang tidak sah terdeteksi, seperti pengguna yang tidak terdaftar mencoba memberikan suara atau pengguna mencoba memberikan suara lagi, peristiwa UnauthorizedAttempt
akan dipancarkan.
Secara keseluruhan, fungsi-fungsi, status, konstanta-konstanta, dan peristiwa-peristiwa ini menciptakan sistem Voting terstruktur, mengelola siklus hidup suara dari pendaftaran hingga pemungutan, pencatatan peristiwa, dan pengambilan hasil dalam lingkungan Starknet. Konstanta-konstanta seperti YES
dan NO
membantu memperlancar proses Voting, sementara peristiwa-peristiwa memainkan peran penting dalam memastikan transparansi dan penelusuran.
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_12_vote_contract/src/lib.cairo}}
Kontrak pintar Voting
Penyebaran, Pemanggilan, dan Memanggil Kontrak Voting
Bagian dari pengalaman Starknet adalah penyebaran dan interaksi dengan kontrak pintar.
Setelah kontrak dideploy, kita dapat berinteraksi dengannya dengan memanggil dan memicu fungsi-fungsi kontrak:
- Memanggil kontrak: Berinteraksi dengan fungsi eksternal yang hanya membaca dari status. Fungsi-fungsi ini tidak mengubah status jaringan, sehingga tidak memerlukan biaya atau tanda tangan.
- Memicu kontrak: Berinteraksi dengan fungsi-fungsi eksternal yang dapat menulis ke status. Fungsi-fungsi ini mengubah status jaringan dan memerlukan biaya serta tanda tangan.
Kita akan menyiapkan sebuah node pengembangan lokal menggunakan katana
untuk melakukan penyebaran kontrak Voting. Selanjutnya, kita akan berinteraksi dengan kontrak tersebut dengan memanggil dan memicu fungsi-fungsinya. Anda juga dapat menggunakan Goerli Testnet sebagai alternatif dari katana
. Namun, kami merekomendasikan penggunaan katana
untuk pengembangan dan pengujian lokal. Anda dapat menemukan tutorial lengkap untuk katana
di Pengembangan Lokal dengan Katana dalam bab Buku Starknet.
Node lokal Starknet katana
katana
dirancang untuk mendukung pengembangan lokal oleh tim Dojo. Ini akan memungkinkan Anda melakukan semua hal yang diperlukan dengan Starknet, tetapi secara lokal. Ini adalah alat yang bagus untuk pengembangan dan pengujian.
Untuk menginstal katana
dari kode sumber, silakan lihat Pengembangan Lokal dengan Katana dalam bab Buku Starknet.
Setelah Anda menginstal katana
, Anda dapat memulai node lokal Starknet dengan:
katana --accounts 3 --seed 0 --gas-price 250
Perintah ini akan memulai node lokal Starknet dengan 3 akun yang telah dideploy. Kami akan menggunakan akun-akun ini untuk menyebar dan berinteraksi dengan kontrak Voting:
...
PREFUNDED ACCOUNTS
==================
| Account address | 0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0
| Private key | 0x0300001800000000300000180000000000030000000000003006001800006600
| Public key | 0x01b7b37a580d91bc3ad4f9933ed61f3a395e0e51c9dd5553323b8ca3942bb44e
| Account address | 0x033c627a3e5213790e246a917770ce23d7e562baa5b4d2917c23b1be6d91961c
| Private key | 0x0333803103001800039980190300d206608b0070db0012135bd1fb5f6282170b
| Public key | 0x04486e2308ef3513531042acb8ead377b887af16bd4cdd8149812dfef1ba924d
| Account address | 0x01d98d835e43b032254ffbef0f150c5606fa9c5c9310b1fae370ab956a7919f5
| Private key | 0x07ca856005bee0329def368d34a6711b2d95b09ef9740ebf2c7c7e3b16c1ca9c
| Public key | 0x07006c42b1cfc8bd45710646a0bb3534b182e83c313c7bc88ecf33b53ba4bcbc
...
Sebelum kita dapat berinteraksi dengan kontrak Voting, kita perlu menyiapkan akun pemilih dan admin di Starknet. Setiap akun pemilih harus didaftarkan dan memiliki dana yang cukup untuk memberikan suara. Untuk pemahaman yang lebih mendetail tentang bagaimana akun beroperasi dengan Abstraksi Akun, lihat bab Abstraksi Akun dalam Buku Starknet.
Smart Wallet untuk Voting
Selain Scarb, Anda juga perlu menginstal Starkli. Starkli adalah alat baris perintah yang memungkinkan Anda berinteraksi dengan Starknet. Anda dapat menemukan petunjuk instalasinya dalam bab Penyiapan Lingkungan dalam Buku Starknet.
Untuk setiap Smart Wallet yang akan kita gunakan, kita harus membuat Signer di dalam keystore terenkripsi dan Deskriptor Akun. Proses ini juga dijelaskan dalam bab Penyiapan Lingkungan dalam Buku Starknet.
Kita dapat membuat Signer dan Deskriptor Akun untuk akun-akun yang ingin kita gunakan untuk memberikan suara. Mari buat Smart Wallet untuk memberikan suara dalam kontrak pintar kita.
Pertama, kita buat seorang signer dari sebuah kunci privat:
starkli signer keystore from-key ~/.starkli-wallets/deployer/account0_keystore.json
Kemudian, kita buat Deskriptor Akun dengan mengambil akun katana yang ingin kita gunakan:
starkli account fetch <Address AKUN KATANA> --rpc http://0.0.0.0:5050 --output ~/.starkli-wallets/deployer/account0_account.json
Perintah ini akan membuat file account0_account.json
yang berisi detail-detail berikut:
{
"version": 1,
"variant": {
"type": "open_zeppelin",
"version": 1,
"public_key": "<KUNCI_PUBLIK_Wallet_CERDAS>"
},
"deployment": {
"status": "deployed",
"class_hash": "<HASH_KELAS_Wallet_CERDAS>",
"address": "<Address_Wallet_CERDAS>"
}
}
Anda dapat mengambil hash dari Smart Wallet (akan sama untuk semua Smart Wallet Anda) dengan perintah berikut. Perhatikan penggunaan flag --rpc
dan ujung titik RPC yang disediakan oleh katana
:
starkli class-hash-at <Address_Wallet_CERDAS> --rpc http://0.0.0.0:5050
Untuk kunci publik, Anda dapat menggunakan perintah starkli signer keystore inspect
dengan direktori file JSON keystore:
starkli signer keystore inspect ~/.starkli-wallets/deployer/account0_keystore.json
Proses ini identik untuk account_1
dan account_2
jika Anda ingin memiliki pemilih kedua dan ketiga.
Penyebaran Kontrak
Sebelum melakukan penyebaran, kita perlu mendeklarasikan kontrak. Kita bisa melakukannya dengan perintah starkli declare
:
starkli declare target/dev/starknetbook_chapter_2_Vote.sierra.json --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account0_account.json --keystore ~/.starkli-wallets/deployer/account0_keystore.json
Jika versi kompiler yang Anda gunakan lebih lama dari yang digunakan oleh Starkli dan Anda mengalami kesalahan compiler-version
saat menggunakan perintah di atas, Anda dapat menentukan versi kompiler yang akan digunakan dalam perintah dengan menambahkan flag --compiler-version x.y.z
.
Jika Anda masih mengalami masalah dengan versi kompiler, cobalah tingkatkan Starkli menggunakan perintah: starkliup
untuk memastikan Anda menggunakan versi terbaru dari starkli.
Hash kelas dari kontrak adalah: 0x06974677a079b7edfadcd70aa4d12aac0263a4cda379009fca125e0ab1a9ba52
. Anda dapat menemukannya di eksplorasi blok apa pun.
Flag --rpc
menentukan ujung titik RPC yang akan digunakan (yang disediakan oleh katana
). Flag --account
menentukan akun yang akan digunakan untuk menandatangani transaksi. Akun yang kita gunakan di sini adalah yang kita buat pada langkah sebelumnya. Flag --keystore
menentukan file keystore yang akan digunakan untuk menandatangani transaksi.
Karena kita menggunakan node lokal, transaksi akan segera mencapai finalitas. Jika Anda menggunakan Goerli Testnet, Anda perlu menunggu transaksi menjadi final, yang biasanya memerlukan beberapa detik.
Perintah berikut ini akan menyebar kontrak Voting dan mendaftarkan voter_0, voter_1, dan voter_2 sebagai pemilih yang memenuhi syarat. Ini adalah argumen konstruktor, jadi tambahkan akun pemilih yang nantinya bisa memberikan suara.
starkli deploy <class_hash_of_the_contract_to_be_deployed> <voter_0_address> <voter_1_address> <voter_2_address> --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account0_account.json --keystore ~/.starkli-wallets/deployer/account0_keystore.json
Contoh command:
starkli deploy 0x06974677a079b7edfadcd70aa4d12aac0263a4cda379009fca125e0ab1a9ba52 0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0 0x033c627a3e5213790e246a917770ce23d7e562baa5b4d2917c23b1be6d91961c 0x01d98d835e43b032254ffbef0f150c5606fa9c5c9310b1fae370ab956a7919f5 --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account0_account.json --keystore ~/.starkli-wallets/deployer/account0_keystore.json
Pada kasus ini, kontrak telah dideploy di Address tertentu: 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349
. Address ini akan berbeda untuk Anda. Kita akan menggunakan Address ini untuk berinteraksi dengan kontrak.
Verifikasi Kelayakan Pemilih
Dalam kontrak Voting kita, ada dua fungsi untuk memvalidasi kelayakan pemilih, yaitu voter_can_vote
dan is_voter_registered
. Fungsi-fungsi ini adalah fungsi baca eksternal, yang berarti mereka tidak mengubah status kontrak tetapi hanya membaca status saat ini.
Fungsi is_voter_registered
memeriksa apakah Address tertentu terdaftar sebagai pemilih yang memenuhi syarat dalam kontrak. Sementara itu, fungsi voter_can_vote
memeriksa apakah pemilih di Address tertentu saat ini berhak memberikan suara, yaitu mereka terdaftar dan belum memberikan suara.
Anda dapat memanggil fungsi-fungsi ini menggunakan perintah starkli call
. Perhatikan bahwa perintah call
digunakan untuk fungsi baca, sementara perintah invoke
digunakan untuk fungsi yang juga dapat menulis ke penyimpanan. Perintah call
tidak memerlukan tanda tangan, sedangkan perintah invoke
memerlukannya.
starkli call 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 voter_can_vote 0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0 --rpc http://0.0.0.0:5050
Pertama, kita tambahkan Address kontrak, kemudian fungsi yang ingin kita panggil, dan terakhir masukan untuk fungsi tersebut. Dalam kasus ini, kita sedang memeriksa apakah pemilih pada Address 0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0
bisa memberikan suara.
Karena kami memberikan Address pemilih yang terdaftar sebagai masukan, hasilnya adalah 1 (benar), yang menunjukkan bahwa pemilih berhak memberikan suara.
Selanjutnya, mari panggil fungsi is_voter_registered
menggunakan Address akun yang tidak terdaftar untuk melihat keluarannya:
starkli call 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 is_voter_registered 0x44444444444444444 --rpc http://0.0.0.0:5050
Dengan Address akun yang tidak terdaftar, output terminalnya adalah 0 (yaitu, salah), yang mengkonfirmasi bahwa akun tersebut tidak memenuhi syarat untuk memberikan suara.
Memberikan Suara
Sekarang setelah kita memahami cara memverifikasi kelayakan pemilih, kita bisa memberikan suara! Untuk memberikan suara, kita berinteraksi dengan fungsi vote
, yang ditandai sebagai eksternal, yang memerlukan penggunaan perintah starknet invoke
.
Syntax perintah invoke
mirip dengan perintah call
, tetapi untuk memberikan suara, kita mengirimkan entah 1
(untuk Ya) atau 0
(untuk Tidak) sebagai masukan. Ketika kita memanggil fungsi vote
, kita akan dikenai biaya, dan transaksi harus ditandatangani oleh pemilih; kita sedang menulis ke penyimpanan kontrak.
//Memberikan suara Ya
starkli invoke 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 vote 1 --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account0_account.json --keystore ~/.starkli-wallets/deployer/account0_keystore.json
//Memberikan suara Tidak
starkli invoke 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 vote 0 --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account0_account.json --keystore ~/.starkli-wallets/deployer/account0_keystore.json
Anda akan diminta untuk memasukkan kata sandi untuk signer. Setelah Anda memasukkan kata sandi, transaksi akan ditandatangani dan dikirimkan ke jaringan Starknet. Anda akan menerima hash transaksi sebagai keluaran. Dengan perintah starkli transaction, Anda dapat mendapatkan lebih banyak detail tentang transaksi tersebut:
starkli transaction <TRANSACTION_HASH> --rpc http://0.0.0.0:5050
Ini akan menampilkan:
{
"transaction_hash": "0x5604a97922b6811060e70ed0b40959ea9e20c726220b526ec690de8923907fd",
"max_fee": "0x430e81",
"version": "0x1",
"signature": [
"0x75e5e4880d7a8301b35ff4a1ed1e3d72fffefa64bb6c306c314496e6e402d57",
"0xbb6c459b395a535dcd00d8ab13d7ed71273da4a8e9c1f4afe9b9f4254a6f51"
],
"nonce": "0x3",
"type": "INVOKE",
"sender_address": "0x3ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0",
"calldata": [
"0x1",
"0x5ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349",
"0x132bdf85fc8aa10ac3c22f02317f8f53d4b4f52235ed1eabb3a4cbbe08b5c41",
"0x0",
"0x1",
"0x1",
"0x1"
]
}
Jika Anda mencoba memberikan suara dua kali dengan signer yang sama, Anda akan mendapatkan kesalahan:
Error: code=ContractError, message="Contract error"
Kesalahan tersebut tidak memberikan informasi yang sangat informatif, tetapi Anda dapat memperoleh lebih banyak detail ketika melihat keluaran di terminal tempat Anda memulai katana
(node Starknet lokal kita):
...
Transaction execution error: "Error in the called contract (0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0):
Error at pc=0:81:
Got an exception while executing a hint: Custom Hint Error: Execution failed. Failure reason: \"USER_ALREADY_VOTED\".
...
Kunci untuk kesalahan tersebut adalah USER_ALREADY_VOTED
.
assert(can_vote == true, 'USER_ALREADY_VOTED');
Kita dapat mengulangi proses untuk membuat Signer dan Account Descriptor untuk akun yang ingin kita gunakan untuk memberikan suara. Ingatlah bahwa setiap Signer harus dibuat dari sebuah private key, dan setiap Account Descriptor harus dibuat dari public key, Address smart wallet, dan smart wallet class hash (yang sama untuk setiap pemilih).
starkli invoke 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 vote 0 --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account1_account.json --keystore ~/.starkli-wallets/deployer/account1_keystore.json
starkli invoke 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 vote 1 --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account2_account.json --keystore ~/.starkli-wallets/deployer/account2_keystore.json
Visualisasi Hasil Voting
Untuk memeriksa hasil Voting, kita memanggil fungsi get_vote_status
, fungsi tampilan lainnya, melalui perintah starknet call
.
starkli call 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 get_vote_status --rpc http://0.0.0.0:5050
Keluarannya menampilkan perolehan suara "Ya" dan "Tidak" beserta persentase relatifnya.
Pesan L1-L2
Fitur penting dari Layer 2 adalah kemampuannya untuk berinteraksi dengan Layer 1.
Starknet memiliki sistem L1-L2
Messaging sendiri, yang berbeda dari mekanisme konsensusnya dan pengiriman pembaruan status di L1. Messaging adalah cara bagi smart-contract di L1 untuk berinteraksi dengan smart-contract di L2 (atau sebaliknya), memungkinkan kita melakukan transaksi "cross-chain". Sebagai contoh, kita dapat melakukan beberapa komputasi pada suatu rantai dan menggunakan hasil komputasi ini pada rantai lain.
Semua jembatan di Starknet menggunakan L1-L2
messaging. Katakanlah Anda ingin menjembatani token dari Ethereum ke Starknet. Anda hanya perlu mendepositkan token Anda di kontrak jembatan L1, yang secara otomatis akan memicu pencetakan token yang sama di L2. Penggunaan kasus lain yang bagus untuk L1-L2
messaging adalah pengelolaan DeFi.
Di Starknet, penting untuk dicatat bahwa sistem messaging ini asynchronous dan asymmetric.
- Asynchronous: ini berarti bahwa dalam kode kontrak Anda (baik itu solidity atau cairo), Anda tidak dapat menunggu hasil dari pesan yang dikirim di rantai lain dalam eksekusi kode kontrak Anda.
- Asymmetric: mengirim pesan dari Ethereum ke Starknet (
L1->L2
) sepenuhnya diotomatisasi oleh sequencer Starknet, yang berarti pesan tersebut secara otomatis dikirim ke kontrak target di L2. Namun, saat mengirim pesan dari Starknet ke Ethereum (L2->L1
), hanya hash dari pesan yang dikirim di L1 oleh sequencer Starknet. Anda kemudian harus mengonsumsi pesan tersebut secara manual melalui transaksi di L1.
Mari kita telusuri detailnya.
Kontrak StarknetMessaging
Komponen penting dari sistem L1-L2
Messaging adalah kontrak StarknetCore
. Ini adalah kumpulan kontrak Solidity yang didistribusikan di Ethereum yang memungkinkan Starknet untuk berfungsi dengan baik. Salah satu kontrak dari StarknetCore
disebut StarknetMessaging
dan merupakan kontrak yang bertanggung jawab atas pengiriman pesan antara Starknet dan Ethereum. StarknetMessaging
mengikuti antarmuka dengan fungsi-fungsi yang memungkinkan pengiriman pesan ke L2, menerima pesan di L1 dari L2, dan membatalkan pesan.
interface IStarknetMessaging is IStarknetMessagingEvents {
function sendMessageToL2(
uint256 toAddress,
uint256 selector,
uint256[] calldata payload
) external returns (bytes32);
function consumeMessageFromL2(uint256 fromAddress, uint256[] calldata payload)
external
returns (bytes32);
function startL1ToL2MessageCancellation(
uint256 toAddress,
uint256 selector,
uint256[] calldata payload,
uint256 nonce
) external;
function cancelL1ToL2Message(
uint256 toAddress,
uint256 selector,
uint256[] calldata payload,
uint256 nonce
) external;
}
Antarmuka kontrak messaging Starknet
Dalam kasus pesan L1->L2
, sequencer Starknet terus-menerus mendengarkan log yang dihasilkan oleh kontrak StarknetMessaging
di Ethereum. Begitu sebuah pesan terdeteksi dalam log, sequencer menyiapkan dan mengeksekusi L1HandlerTransaction
untuk memanggil fungsi di kontrak L2 yang dituju. Proses ini memakan waktu 1-2 menit (beberapa detik untuk blok Ethereum ditambang, dan kemudian sequencer harus membangun dan mengeksekusi transaksi).
Pesan L2->L1
disiapkan oleh eksekusi kontrak di L2 dan merupakan bagian dari blok yang dihasilkan. Ketika sequencer menghasilkan blok, ia mengirim hash setiap pesan yang disiapkan oleh eksekusi kontrak
ke kontrak StarknetCore
di L1, di mana pesan tersebut kemudian dapat dikonsumsi setelah blok yang mereka miliki terbukti dan diverifikasi di Ethereum (saat ini sekitar 3-4 jam).
Mengirim pesan dari Ethereum ke Starknet
Jika Anda ingin mengirim pesan dari Ethereum ke Starknet, kontrak Solidity Anda harus memanggil fungsi sendMessageToL2
dari kontrak StarknetMessaging
. Untuk menerima pesan-pesan ini di Starknet, Anda perlu menandai fungsi-fungsi yang dapat dipanggil dari L1 dengan atribut #[l1_handler]
.
Mari kita lihat sebuah kontrak sederhana yang diambil dari tutorial ini di mana kita ingin mengirim pesan ke Starknet.
_snMessaging
adalah variabel state yang sudah diinisialisasi dengan Address kontrak StarknetMessaging
. Anda dapat memeriksa Address-Address tersebut di sini.
// Mengirim pesan di Starknet dengan satu nilai felt.
function sendMessageFelt(
uint256 contractAddress,
uint256 selector,
uint256 myFelt
)
external
payable
{
// Kami "serialize" nilai felt ke dalam payload, yang merupakan array uint256.
uint256[] memory payload = new uint256[](1);
payload[0] = myFelt;
// msg.value harus selalu >= 20_000 wei.
_snMessaging.sendMessageToL2{value: msg.value}(
contractAddress,
selector,
payload
);
}
Fungsi ini mengirim pesan dengan satu nilai felt ke kontrak StarknetMessaging
.
Harap dicatat bahwa jika Anda ingin mengirim data yang lebih kompleks, Anda dapat melakukannya. Hanya perhatikan bahwa kontrak cairo Anda hanya akan memahami tipe data felt252
. Jadi Anda harus memastikan bahwa serialisasi data Anda ke dalam array uint256
mengikuti skema serialisasi cairo.
Penting untuk dicatat bahwa kita memiliki {value: msg.value}
. Faktanya, nilai minimum yang harus kami kirim di sini adalah 20k wei
, karena kontrak StarknetMessaging
akan mendaftarkan
hash pesan kita dalam penyimpanan Ethereum.
Selain dari 20k wei
itu, karena L1HandlerTransaction
yang akan dieksekusi oleh sequencer tidak terikat pada akun mana pun (pesan berasal dari L1), Anda juga harus memastikan
bahwa Anda membayar cukup biaya di L1 agar pesan Anda dapat didekripsi dan diproses di L2.
Biaya dari L1HandlerTransaction
dihitung secara regular seperti halnya untuk transaksi Invoke
. Untuk ini, Anda dapat memperkirakan konsumsi gas menggunakan starkli
atau snforge
untuk mengetahui biaya eksekusi pesan Anda.
Tanda tangan dari sendMessageToL2
adalah:
function sendMessageToL2(
uint256 toAddress,
uint256 selector,
uint256[] calldata payload
) external override returns (bytes32);
Parameter-parameter tersebut adalah sebagai berikut:
toAddress
: Address kontrak di L2 yang akan dipanggil.selector
: Selektor dari fungsi kontrak ini ditoAddress
. Selektor (fungsi) ini harus memiliki atribut#[l1_handler]
agar dapat dipanggil.payload
: Payload selalu berupa arrayfelt252
(yang direpresentasikan olehuint256
dalam Solidity). Untuk alasan ini, kami telah memasukkan inputmyFelt
ke dalam array. Inilah sebabnya mengapa kita perlu memasukkan data input ke dalam sebuah array.
Di sisi Starknet, untuk menerima pesan ini, kita memiliki:
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_04_L1_L2_messaging/src/lib.cairo:felt_msg_handler}}
Kita perlu menambahkan atribut #[l1_handler]
ke dalam fungsi kita. Handler L1 adalah fungsi khusus yang hanya dapat dieksekusi oleh L1HandlerTransaction
. Tidak ada yang perlu dilakukan secara khusus untuk menerima transaksi dari L1, karena pesan tersebut secara otomatis diteruskan oleh sequencer. Pada fungsi #[l1_handler]
Anda, penting untuk memverifikasi pengirim pesan L1 untuk memastikan bahwa kontrak kita hanya dapat menerima pesan dari kontrak L1 yang terpercaya.
Mengirim pesan dari Starknet ke Ethereum
Ketika mengirim pesan dari Starknet ke Ethereum, Anda harus menggunakan syscall send_message_to_l1
dalam kontrak Cairo Anda. Syscall ini memungkinkan Anda untuk mengirim pesan ke kontrak StarknetMessaging
di L1. Berbeda dengan pesan L1->L2
, pesan L2->L1
harus dikonsumsi secara manual, yang berarti bahwa Anda perlu kontrak Solidity Anda untuk memanggil fungsi consumeMessageFromL2
dari kontrak StarknetMessaging
secara eksplisit untuk mengonsumsi pesan tersebut.
Untuk mengirim pesan dari L2 ke L1, apa yang akan kita lakukan di Starknet adalah:
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_04_L1_L2_messaging/src/lib.cairo:felt_msg_send}}
Kami hanya membangun payload dan meneruskannya, bersama dengan Address kontrak L1, ke fungsi syscall.
Pada L1, bagian penting adalah untuk membangun payload yang sama seperti pada L2. Kemudian Anda memanggil consumeMessageFromL2
dengan melewati Address kontrak L2 dan payload.
Harap diperhatikan bahwa Address kontrak L2 yang diharapkan oleh consumeMessageFromL2
adalah Address kontrak dari akun yang mengirim transaksi di L2,
dan bukan Address kontrak yang menjalankan send_message_to_l1_syscall
.
function consumeMessageFelt(
uint256 fromAddress,
uint256[] calldata payload
)
external
{
let messageHash = _snMessaging.consumeMessageFromL2(fromAddress, payload);
// Anda dapat menggunakan hash pesan di sini jika diinginkan.
// Kami mengharapkan payload hanya berisi nilai felt252 (yang merupakan uint256 dalam Solidity).
require(payload.length == 1, "Payload tidak valid");
uint256 my_felt = payload[0];
// Dari sini, Anda dapat dengan aman menggunakan `my_felt` karena pesan telah diverifikasi oleh StarknetMessaging.
require(my_felt > 0, "Nilai tidak valid");
}
Seperti yang terlihat, dalam konteks ini, kita tidak perlu memverifikasi kontrak mana dari L2 yang mengirim pesan. Namun, kita sebenarnya menggunakan consumeMessageFromL2
untuk memvalidasi input (Address pengirim di L2 dan payload) untuk memastikan kita hanya mengonsumsi pesan yang valid.
Penting untuk diingat bahwa di L1 kita mengirim payload uint256
, namun tipe data dasar di Starknet adalah felt252
; namun, felt252
sekitar 4 bit lebih kecil dari uint256
. Jadi kita harus memperhatikan nilai yang terkandung dalam payload dari pesan yang kita kirim. Jika, di L1, kita membangun pesan dengan nilai di atas maksimum felt252
, pesan tersebut akan terjebak dan tidak pernah dikonsumsi di L2.
Cairo Serde
Sebelum mengirim pesan antara L1 dan L2, Anda harus ingat bahwa kontrak Starknet, yang ditulis dalam Cairo, hanya dapat memahami data yang telah diserialkan. Dan data yang diserialkan selalu berupa array felt252
.
Pada Solidity, kita memiliki tipe uint256
, dan felt252
sekitar 4 bit lebih kecil dari uint256
. Jadi kita harus memperhatikan nilai-nilai yang terkandung dalam payload pesan yang kita kirim.
Jika, di L1, kita membangun pesan dengan nilai di atas maksimum felt252
, pesan tersebut akan terjebak dan tidak pernah dikonsumsi di L2.
Sebagai contoh, nilai uint256
sebenarnya dalam Cairo direpresentasikan oleh sebuah struktur seperti:
struct u256 {
rendah: u128,
tinggi: u128,
}
yang akan diserialkan sebagai DUA felt, satu untuk rendah
dan satu untuk tinggi
. Ini berarti untuk mengirim hanya satu u256
ke Cairo, Anda harus mengirim payload dari L1 dengan DUA nilai.
uint256[] memory payload = new uint256[](2);
// Mari kirim nilai 1 sebagai u256 di Cairo: rendah = 1, tinggi = 0.
payload[0] = 1;
payload[1] = 0;
Jika Anda ingin mempelajari lebih lanjut tentang mekanisme pengiriman pesan, Anda dapat mengunjungi dokumentasi Starknet.
Anda juga dapat menemukan panduan terperinci di sini untuk menguji pengiriman pesan secara lokal.
Pertimbangan Keamanan
Ketika mengembangkan perangkat lunak, memastikan bahwa perangkat lunak tersebut berfungsi sebagaimana yang dimaksud biasanya cukup mudah. Namun, mencegah penggunaan yang tidak diinginkan dan kerentanan bisa menjadi lebih menantang.
Dalam pengembangan kontrak pintar (smart contract), keamanan sangatlah penting. Sebuah kesalahan kecil saja dapat mengakibatkan kehilangan aset berharga atau berfungsinya fitur-fitur tertentu secara tidak benar.
Kontrak pintar dijalankan dalam lingkungan publik di mana siapa pun dapat memeriksa kode dan berinteraksi dengannya. Setiap kesalahan atau kerentanan dalam kode dapat dieksploitasi oleh pihak yang jahat.
Bab ini menyajikan rekomendasi umum untuk menulis kontrak pintar yang aman. Dengan menggabungkan konsep-konsep ini selama pengembangan, Anda dapat membuat kontrak pintar yang kuat dan dapat diandalkan. Hal ini mengurangi kemungkinan perilaku atau kerentanan yang tidak terduga.
Penyangkalan
Bab ini tidak memberikan daftar lengkap dari semua kemungkinan masalah keamanan, dan tidak menjamin bahwa kontrak Anda akan sepenuhnya aman.
Jika Anda sedang mengembangkan kontrak pintar untuk digunakan secara produksi, sangat disarankan untuk melakukan audit eksternal yang dilakukan oleh ahli keamanan.
Pola Pikir
Cairo adalah bahasa yang sangat aman yang terinspirasi oleh Rust. Dirancang sedemikian rupa untuk memaksa Anda menutupi semua kemungkinan kasus. Masalah keamanan pada Starknet sebagian besar muncul dari cara alur kontrak pintar dirancang, bukan begitu banyak dari bahasa itu sendiri.
Mengadopsi pola pikir keamanan adalah langkah awal dalam menulis kontrak pintar yang aman. Cobalah untuk selalu mempertimbangkan semua skenario yang mungkin saat menulis kode.
Melihat Kontrak Pintar sebagai Mesin Keadaan Terbatas
Transaksi dalam kontrak pintar bersifat atomik, artinya mereka either berhasil atau gagal tanpa membuat perubahan apapun.
Pikirkan kontrak pintar sebagai mesin keadaan: mereka memiliki kumpulan keadaan awal yang ditentukan oleh batasan konstruktor, dan fungsi eksternal mewakili kumpulan transisi keadaan yang mungkin. Sebuah transaksi hanyalah sebuah transisi keadaan.
Fungsi assert
atau panic
dapat digunakan untuk memvalidasi kondisi sebelum melakukan tindakan tertentu. Anda dapat mempelajari lebih lanjut mengenai hal ini di halaman Unrecoverable Errors with panic.
Validasi ini bisa mencakup:
- Masukan yang diberikan oleh pemanggil
- Persyaratan eksekusi
- Invarian (kondisi yang harus selalu benar)
- Nilai kembalian dari panggilan fungsi lainnya
Sebagai contoh, Anda dapat menggunakan fungsi assert
untuk memvalidasi bahwa seorang pengguna memiliki cukup dana untuk melakukan transaksi penarikan. Jika kondisinya tidak terpenuhi, transaksi akan gagal dan keadaan kontrak tidak akan berubah.
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_10_assert_balance/src/lib.cairo:withdraw}}
Menggunakan fungsi-fungsi ini untuk memeriksa kondisi menambahkan batasan yang membantu menentukan dengan jelas batas-batas transisi keadaan yang mungkin untuk setiap fungsi dalam kontrak pintar Anda. Pemeriksaan ini memastikan bahwa perilaku kontrak tetap berada dalam batas yang diharapkan.
Rekomendasi
Pola Checks Effects Interactions
Pola Checks Effects Interactions adalah pola desain umum yang digunakan untuk mencegah serangan reentrancy pada Ethereum. Meskipun reentrancy lebih sulit dicapai di Starknet, masih disarankan untuk menggunakan pola ini dalam kontrak pintar Anda.
Pola ini terdiri dari mengikuti urutan operasi tertentu dalam fungsi-fungsi Anda:
- Pengecekan: Memvalidasi semua kondisi dan masukan sebelum melakukan perubahan keadaan apapun.
- Efek: Melakukan semua perubahan keadaan.
- Interaksi: Semua panggilan eksternal ke kontrak lain sebaiknya dilakukan di akhir fungsi.
Kontrol Akses
Kontrol akses adalah proses membatasi akses ke fitur atau sumber daya tertentu. Ini adalah mekanisme keamanan umum yang digunakan untuk mencegah akses tidak sah ke informasi atau tindakan sensitif. Dalam kontrak pintar, beberapa fungsi mungkin sering dibatasi kepada pengguna atau peran tertentu.
Anda dapat menerapkan pola kontrol akses untuk mengelola izin dengan mudah. Pola ini terdiri dari mendefinisikan kumpulan peran dan menetapkannya kepada pengguna tertentu. Setiap fungsi kemudian dapat dibatasi kepada peran-peran tertentu.
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_11_simple_access_control/src/lib.cairo}}
Alat Analisis Statis
Analisis statis merujuk pada proses pemeriksaan kode tanpa eksekusinya, berfokus pada struktur, sintaksis, dan properti kode tersebut. Ini melibatkan analisis kode sumber untuk mengidentifikasi masalah potensial, kerentanan, atau pelanggaran aturan yang ditentukan.
Dengan mendefinisikan aturan, seperti konvensi penulisan kode atau pedoman keamanan, pengembang dapat menggunakan alat analisis statis untuk secara otomatis memeriksa kode sesuai dengan standar-standar tersebut.
Referensi:
Lampiran
Bagian berikut berisi bahan referensi yang mungkin berguna bagi Perjalanan Cairo Anda.
Lampiran A: Kata Kunci
Daftar berikut berisi kata kunci yang diperuntukkan bagi penggunaan bahasa Kairo saat ini atau di masa mendatang.
Ada dua kategori kata kunci:
- ketat
- longgar
- disimpan
Ada kategori ketiga, yaitu fungsi dari perpustakaan inti. Meskipun namanya tidak dicadangkan, namun tidak disarankan untuk digunakan sebagai nama item apa pun untuk mengikuti praktik yang baik.
Kata kunci yang ketat
Kata kunci ini hanya dapat digunakan dalam konteks yang benar. Mereka tidak dapat digunakan sebagai nama item apa pun.
as
- Ganti nama imporbreak
- segera keluar dari loopconst
- Tentukan item konstancontinue
- Lanjutkan ke iterasi loop berikutnyaelse
- Penggantian untuk konstruksi aliran kontrolif
danif let
enum
- Tentukan enumerasiextern
- Fungsi yang ditentukan pada tingkat kompiler menggunakan petunjuk yang tersedia di tingkat Kairo1 dengan deklarasi inifalse
- Boolean salah secara literalfn
- Mendefinisikan suatu fungsiif
- Cabang berdasarkan hasil ekspresi kondisionalimpl
- Menerapkan fungsionalitas bawaan atau sifatimplicits
- Jenis parameter fungsi khusus yang diperlukan untuk melakukan tindakan tertentulet
- Ikat variabelloop
- Ulangi tanpa syaratmatch
- Cocokkan nilai dengan polamod
- Tentukan modulmut
- Menunjukkan mutabilitas variabelnopanic
- Fungsi yang ditandai dengan notasi ini berarti fungsi tersebut tidak akan pernah panik.of
- Implementasi suatu sifatref
- Parameter yang diteruskan secara implisit dikembalikan pada akhir suatu fungsireturn
- Kembali dari fungsistruct
- Tentukan strukturtrait
- Tentukan suatu sifattrue
- Boolean benar secara literaltype
- Tentukan alias tipeuse
- Membawa simbol ke dalam ruang lingkup
Kata Kunci Longgar
Kata kunci ini dikaitkan dengan perilaku tertentu, namun juga dapat digunakan untuk mendefinisikan item.
self
- Subjek metodesuper
- Modul induk dari modul saat ini
Kata kunci yang dipesan
Kata kunci ini belum digunakan, namun dicadangkan untuk penggunaan di masa mendatang. Kata kunci tersebut memiliki batasan yang sama dengan kata kunci ketat. Alasan dibalik hal ini adalah untuk membuat program yang ada saat ini kompatibel dengan versi Kairo yang akan datang dengan melarang mereka menggunakan kata kunci tersebut.
Self
assert
do
dyn
for
hint
in
macro
move
pub
static_assert
self
static
super
try
typeof
unsafe
where
while
with
yield
Fungsi bawaan
Bahasa pemrograman Kairo menyediakan beberapa fungsi spesifik yang memiliki tujuan khusus. Kami tidak akan membahas semuanya dalam buku ini, tetapi tidak disarankan menggunakan nama fungsi ini sebagai nama item lainnya.
- assert
- Fungsi ini memeriksa ekspresi boolean, dan jika bernilai salah, fungsi panik akan dipicu. - panic
- Fungsi ini menghentikan program.
Lampiran B: Operator dan Simbol
Lampiran ini mencakup glosarium tentang sintaksis Cairo.
Operator
Tabel B-1 berisi operator dalam bahasa Cairo, contoh penggunaan operator dalam konteks, penjelasan singkat, dan apakah operator tersebut dapat di-overload. Jika suatu operator dapat di-overload, maka dituliskan trait yang relevan untuk meng-overload operator tersebut.
Tabel B-1: Operator
Operator | Contoh | Penjelasan | Dapat Di-Overload? |
---|---|---|---|
! | !expr | Komplemen bitwise atau logika | Not |
!= | expr != expr | Perbandingan ketidaksetaraan | PartialEq |
% | expr % expr | Sisa pembagian aritmatika | Rem |
%= | var %= expr | Sisa pembagian aritmatika dan assignment | RemEq |
& | expr & expr | AND bitwise | BitAnd |
&& | expr && expr | AND logika dengan short-circuiting | |
* | expr * expr | Perkalian aritmatika | Mul |
*= | var *= expr | Perkalian aritmatika dan assignment | MulEq |
@ | @var | Snapshot | |
* | *var | Desnap | |
+ | expr + expr | Penambahan aritmatika | Add |
+= | var += expr | Penambahan aritmatika dan assignment | AddEq |
, | expr, expr | Pemisah argumen dan elemen | |
- | -expr | Penyangkalan aritmatika | Neg |
- | expr - expr | Pengurangan aritmatika | Sub |
-= | var -= expr | Pengurangan aritmatika dan assignment | SubEq |
-> | fn(...) -> type , |...| -> type | Tipe kembalian fungsi dan closure | |
. | expr.ident | Akses anggota | |
/ | expr / expr | Pembagian aritmatika | Div |
/= | var /= expr | Pembagian aritmatika dan assignment | DivEq |
: | pat: type , ident: type | Batasan | |
: | ident: expr | Inisialisasi field struct | |
; | expr; | Penutup pernyataan dan item | |
< | expr < expr | Perbandingan kurang dari | PartialOrd |
<= | expr <= expr | Perbandingan kurang dari atau sama dengan | PartialOrd |
= | var = expr | Assignment | |
== | expr == expr | Perbandingan kesetaraan | PartialEq |
=> | pat => expr | Bagian dari sintaksis lengan match | |
> | expr > expr | Perbandingan lebih dari | PartialOrd |
>= | expr >= expr | Perbandingan lebih dari atau sama dengan | PartialOrd |
^ | expr ^ expr | XOR bitwise | BitXor |
| | expr | expr | OR bitwise | BitOr |
|| | expr || expr | OR logika dengan short-circuiting |
Simbol Non Operator
Berikut ini adalah daftar semua simbol yang bukan digunakan sebagai operator; dengan kata lain, simbol-simbol ini tidak memiliki perilaku yang sama seperti pemanggilan fungsi atau metode.
Tabel B-2 menunjukkan simbol-simbol yang muncul sendiri dan valid dalam berbagai lokasi.
Tabel B-2: Sintaksis Tunggal
Simbol | Penjelasan |
---|---|
..._u8 , ..._usize , dll. | Literal numerik dari tipe tertentu |
'...' | String pendek |
_ | Pola ikut-aba; juga digunakan untuk membuat literal bilangan bulat mudah dibaca |
Tabel B-3 menunjukkan simbol-simbol yang digunakan dalam konteks jalur hierarki modul untuk mengakses suatu item.
Tabel B-3: Sintaksis Terkait Jalur
Simbol | Penjelasan |
---|---|
ident::ident | Jalur namespace |
super::path | Jalur relatif terhadap induk modul saat ini |
trait::method(...) | Mendisambiguasi pemanggilan metode dengan menyebutkan trait yang mendefinisikannya |
Tabel B-4 menunjukkan simbol-simbol yang muncul dalam konteks penggunaan parameter tipe generik.
Tabel B-4: Generik
Simbol | Penjelasan |
---|---|
path<...> | Menentukan parameter untuk tipe generik dalam suatu tipe (mis., Vec<u8> ) |
path::<...> , method::<...> | Menentukan parameter untuk tipe generik, fungsi, atau metode dalam suatu ekspresi; sering disebut sebagai turbofish |
fn ident<...> ... | Mendefinisikan fungsi generik |
struct ident<...> ... | Mendefinisikan struktur generik |
enum ident<...> ... | Mendefinisikan enumerasi generik |
impl<...> ... | Mendefinisikan implementasi generik |
Tabel B-5 menunjukkan simbol-simbol yang muncul dalam konteks pemanggilan atau definisi makro dan penentuan atribut pada suatu item.
Tabel B-5: Makro dan Atribut
Simbol | Penjelasan |
---|---|
#[meta] | Atribut luar |
Tabel B-6 menunjukkan simbol-simbol yang membuat komentar.
Tabel B-6: Komentar
Simbol | Penjelasan |
---|---|
// | Komentar baris |
Tabel B-7 menunjukkan simbol-simbol yang muncul dalam konteks penggunaan tuple.
Tabel B-7: Tuple
Simbol | Penjelasan |
---|---|
() | Tuple kosong (juga disebut unit), baik literal maupun tipe |
(expr) | Ekspresi dalam tanda kurung |
(expr,) | Ekspresi tuple dengan satu elemen |
(type,) | Tipe tuple dengan satu elemen |
(expr, ...) | Ekspresi tuple |
(type, ...) | Tipe tuple |
expr(expr, ...) | Pemanggilan fungsi; juga digunakan untuk menginisialisasi struct tuple dan varian enum tuple |
Tabel B-8 menunjukkan konteks di mana kurung kurawal digunakan.
Tabel B-8: Kurung Kurawal
Konteks | Penjelasan |
---|---|
{...} | Ekspresi blok |
Type {...} | Literal struct |
Lampiran C: Trait yang Dapat Di-Derive
Di berbagai bagian dalam buku ini, kami telah membahas atribut derive
, yang dapat Anda aplikasikan pada definisi struktur atau enumerasi. Atribut derive
menghasilkan kode untuk menerapkan sebuah trait bawaan pada tipe yang Anda anotasi dengan sintaks derive
.
Pada lampiran ini, kami menyediakan referensi komprehensif yang mendetail tentang semua trait dalam pustaka standar yang kompatibel dengan atribut derive
.
Trait-trait yang terdaftar di sini adalah satu-satunya trait yang didefinisikan oleh pustaka inti yang dapat diimplementasikan pada tipe-tipe Anda menggunakan derive
. Trait lain yang didefinisikan dalam pustaka standar tidak memiliki perilaku default yang masuk akal, sehingga terserah pada Anda untuk mengimplementasikannya sesuai dengan logika yang sesuai dengan tujuan yang ingin Anda capai.
Daftar trait yang dapat di-derive yang disediakan dalam lampiran ini tidak mencakup semua kemungkinan: pustaka eksternal dapat mengimplementasikan derive
untuk trait mereka sendiri, memperluas daftar trait yang kompatibel dengan derive
.
PartialEq untuk Perbandingan Kesetaraan
Trait PartialEq
memungkinkan perbandingan antara instance dari suatu tipe untuk kesetaraan, dengan demikian memungkinkan operator == dan !=.
Ketika PartialEq
di-derive pada struktur, dua instance adalah sama hanya jika semua field sama, dan instance tersebut tidak sama jika ada field yang tidak sama. Ketika di-derive pada enumerasi, setiap varian adalah sama dengan dirinya sendiri dan tidak sama dengan varian lainnya.
Contoh:
#[derive(PartialEq, Drop)]
struct A {
item: felt252
}
fn main() {
let first_struct = A {
item: 2
};
let second_struct = A {
item: 2
};
assert(first_struct == second_struct, 'Struktur berbeda');
}
Clone dan Copy untuk Menduplikasi Nilai
Trait Clone
menyediakan fungsionalitas untuk membuat salinan mendalam (deep copy) dari suatu nilai.
Menerapkan Clone
mengimplementasikan metode clone
, yang pada gilirannya memanggil clone pada setiap komponen tipe tersebut. Ini berarti semua field atau nilai dalam tipe harus juga mengimplementasikan Clone
untuk mendapatkan turunan Clone
.
Contoh:
use clone::Clone;
#[derive(Clone, Drop)]
struct A {
item: felt252
}
fn main() {
let first_struct = A {
item: 2
};
let second_struct = first_struct.clone();
assert(second_struct.item == 2, 'Tidak sama');
}
Copy
trait memungkinkan untuk menduplikasi nilai-nilai. Anda dapat menurunkan Copy
pada jenis apa pun yang bagian-bagiannya semuanya menerapkan Copy
.
Contoh:
#[derive(Copy, Drop)]
struct A {
item: felt252
}
fn main() {
let first_struct = A {
item: 2
};
let second_struct = first_struct;
assert(second_struct.item == 2, 'Not equal');
assert(first_struct.item == 2, 'Not Equal'); // Sifat Copy mencegah first_struct pindah ke second_struct
}
Serialisasi dengan Serde
Serde
menyediakan implementasi trait untuk fungsi serialize
dan deserialize
untuk struktur data yang didefinisikan dalam kreatif Anda. Ini memungkinkan Anda untuk mengubah struktur Anda menjadi larik (atau sebaliknya).
Contoh:
use serde::Serde;
use array::ArrayTrait;
#[derive(Serde, Drop)]
struct A {
item_one: felt252,
item_two: felt252,
}
fn main() {
let first_struct = A {
item_one: 2,
item_two: 99,
};
let mut output_array = ArrayTrait::new();
let serialized = first_struct.serialize(ref output_array);
panic(output_array);
}
Output:
Run panicked with [2 (''), 99 ('c'), ].
Kita bisa melihat di sini bahwa struktur A kita telah di-serialize menjadi array output.
Juga, kita bisa menggunakan fungsi deserialize
untuk mengonversi array yang telah di-serialize kembali ke struktur A kita.
Contoh:
use serde::Serde;
use array::ArrayTrait;
use option::OptionTrait;
#[derive(Serde, Drop)]
struct A {
item_one: felt252,
item_two: felt252,
}
fn main() {
let first_struct = A {
item_one: 2,
item_two: 99,
};
let mut output_array = ArrayTrait::new();
let mut serialized = first_struct.serialize(ref output_array);
let mut span_array = output_array.span();
let deserialized_struct: A = Serde::<A>::deserialize(ref span_array).unwrap();
}
Di sini kita mengonversi kembali array span yang telah di-serialize ke struktur A. deserialize
mengembalikan Option
sehingga kita perlu melakukan unwrap. Ketika menggunakan deserialize, kita juga perlu menentukan tipe yang ingin kita deserialkan ke dalamnya.
Drop dan Destruct
Ketika keluar dari lingkup, variabel perlu dipindahkan terlebih dahulu. Di sinilah sifat Drop
turun. Anda dapat menemukan lebih banyak detail tentang penggunaannya di sini.
Selain itu, Dictionary perlu disusun sebelum keluar dari lingkup. Memanggil secara manual metode squash
pada masing-masing dapat dengan cepat menjadi redundan. Sifat Destruct
memungkinkan Dictionary untuk secara otomatis disusun ketika mereka keluar dari lingkup. Anda juga dapat menemukan informasi lebih lanjut tentang Destruct
di sini.
Simpan
Menyimpan struktur yang ditentukan pengguna dalam variabel penyimpanan dalam kontrak Starknet memerlukan sifat Store
untuk diimplementasikan untuk jenis ini. Anda dapat secara otomatis mendapatkan sifat store
untuk semua struktur yang tidak mengandung jenis kompleks seperti Dictionary atau Array.
Contoh:
#[starknet::contract]
mod contract {
#[derive(Drop, starknet::Store)]
struct A {
item_one: felt252,
item_two: felt252,
}
#[storage]
struct Storage {
my_storage: A,
}
}
Di sini kami menunjukkan implementasi struct A
yang mendapat sifat Store. struct A
ini kemudian digunakan sebagai variabel penyimpanan dalam kontrak.
PartialOrd dan Ord untuk Perbandingan Penyusunan
Selain sifat PartialEq
, pustaka standar juga menyediakan sifat PartialOrd
dan Ord
untuk membandingkan nilai-nilai untuk penyusunan.
Sifat PartialOrd
memungkinkan perbandingan antara instance dari suatu jenis untuk penyusunan, sehingga mengaktifkan operator <, <=, >, dan >=.
Ketika PartialOrd
diturunkan pada struktur, dua instance diurutkan dengan membandingkan setiap bidang secara bergantian.
Lampiran D - Alat Pengembangan yang Berguna
Pada lampiran ini, kita akan membahas beberapa alat pengembangan yang berguna yang disediakan oleh proyek Cairo. Kita akan melihat tentang pemformatan otomatis, cara cepat untuk menerapkan perbaikan peringatan, sebuah linter, dan integrasi dengan IDE.
Pemformatan Otomatis dengan scarb fmt
Proyek-proyek Scarb dapat diformat menggunakan perintah scarb fmt
. Jika Anda menggunakan biner Cairo secara langsung, Anda dapat menjalankan cairo-format
sebagai gantinya. Banyak proyek kolaboratif menggunakan scarb fmt
untuk mencegah adanya argumen tentang gaya mana yang harus digunakan saat menulis kode Cairo: semua orang memformat kode mereka menggunakan alat ini.
Untuk memformat proyek Cairo apa pun, masukkan perintah berikut:
Integrasi IDE Menggunakan cairo-language-server
Untuk membantu integrasi IDE, komunitas Cairo merekomendasikan penggunaan cairo-language-server
. Alat ini adalah serangkaian utilitas yang berpusat pada kompiler yang berbicara dalam Language Server Protocol, yang merupakan spesifikasi untuk IDE dan bahasa pemrograman berkomunikasi satu sama lain. Berbagai klien dapat menggunakan cairo-language-server
, seperti ekstensi Cairo untuk Visual Studio Code.
Kunjungi halaman vscode-cairo
untuk menginstalnya di VSCode. Anda akan mendapatkan kemampuan seperti autocompletion, lompat ke definisi, dan kesalahan inline.
Catatan: Jika Anda telah menginstal Scarb, seharusnya berfungsi dengan baik secara otomatis dengan ekstensi Cairo VSCode, tanpa perlu instalasi manual dari server bahasa.
Lampiran E - Jenis dan Sifat Umum serta Preludium Cairo
Preludium
Preludium Cairo adalah kumpulan modul, fungsi, jenis data, dan sifat yang sering digunakan yang secara otomatis dibawa ke dalam lingkup setiap modul dalam suatu keranjang Cairo tanpa perlu pernyataan impor eksplisit. Preludium Cairo menyediakan blok bangunan dasar yang dibutuhkan pengembang untuk memulai program Cairo dan menulis kontrak pintar.
Preludium pustaka inti didefinisikan dalam berkas lib.cairo dari keranjang corelib dan berisi jenis data primitif, sifat, operator, dan fungsi utilitas Cairo. Ini mencakup: Jenis data - felts, bools, array, dicts, dll. Sifat - perilaku untuk aritmatika, perbandingan, serialisasi. Operator - aritmatika, logis, bitwise. Fungsi utilitas - pembantu untuk array, peta, boxing, dll. Preludium pustaka inti menyampaikan konstruksi pemrograman dan operasi dasar yang diperlukan untuk program Cairo dasar, tanpa memerlukan impor eksplisit dari elemen-elemen tersebut. Karena preludium pustaka inti diimpor secara otomatis, isinya tersedia untuk digunakan dalam keranjang Cairo mana pun tanpa impor eksplisit. Ini mencegah pengulangan dan memberikan pengalaman pengembangan yang lebih baik. Inilah yang memungkinkan Anda menggunakan ArrayTrait::append()
atau sifat Default
tanpa membawanya secara eksplisit ke dalam lingkup.
Anda dapat memilih preludium yang akan digunakan. Misalnya, dengan menambahkan edition = "2023_10"
dalam berkas konfigurasi Scarb.toml
akan memuat preludium dari Oktober 2023, yang lebih terbatas daripada yang dari Januari 2023.
Compiler saat ini mengekspos 2 versi preludium yang berbeda:
- Versi umum, dengan banyak sifat yang tersedia, sesuai dengan
edition = "2023_01"
. - Versi yang lebih terbatas, termasuk sifat-sifat yang paling penting diperlukan untuk pemrograman cairo umum, sesuai dengan
edition = 2023_10
.
Daftar jenis dan sifat umum
Bagian berikut memberikan gambaran singkat tentang jenis dan sifat yang sering digunakan saat mengembangkan program Cairo. Sebagian besar termasuk dalam preludium dan tidak perlu diimpor secara eksplisit - tetapi tidak semua.
Impor | Path | Penggunaan |
---|---|---|
OptionTrait | core::option::OptionTrait | OptionTrait<T> mendefinisikan seperangkat metode yang diperlukan untuk memanipulasi nilai opsional. |
ResultTrait | core::result::ResultTrait | ResultTrait<T, E> Jenis untuk Address kontrak Starknet, nilai dalam rentang [0, 2 ** 251). |
ContractAddress | starknet::ContractAddress | ContractAddress adalah jenis untuk mewakili Address kontrak pintar. |
ContractAddressZeroable | starknet::contract_address::ContractAddressZeroable | ContractAddressZeroable adalah implementasi dari sifat Zeroable untuk jenis ContractAddress . Diperlukan untuk memeriksa apakah nilai t:ContractAddress adalah nol atau tidak. |
contract_address_const | starknet::contract_address_const | contract_address_const! adalah fungsi yang memungkinkan instansiasi nilai Address kontrak konstan. |
Into | traits::Into; | Into<T> adalah sifat yang digunakan untuk konversi antar jenis. Jika ada implementasi Into<T,S> untuk jenis T dan S, Anda dapat mengonversi T menjadi S. |
TryInto | traits::TryInto; | TryInto<T> adalah sifat yang digunakan untuk konversi antar jenis. Jika ada implementasi TryInto<T,S> untuk jenis T dan S, Anda dapat mengonversi T menjadi S. |
get_caller_address | starknet::get_caller_address | get_caller_address() adalah fungsi yang mengembalikan Address pemanggil kontrak. Ini dapat digunakan untuk mengidentifikasi pemanggil fungsi kontrak. |
get_contract_address | starknet::info::get_contract_address | get_contract_address() adalah fungsi yang mengembalikan Address kontrak saat ini. Ini dapat digunakan untuk mendapatkan Address kontrak yang sedang dieksekusi. |
Ini bukan daftar yang lengkap, tetapi mencakup beberapa jenis dan sifat yang sering digunakan dalam pengembangan kontrak. Untuk informasi lebih lanjut, lihat dokumentasi resmi dan jelajahi pustaka dan kerangka kerja yang tersedia.
Lampiran F: Memasang Biner Cairo
Jika Anda ingin mengakses biner Cairo, untuk hal-hal yang tidak dapat Anda capai hanya dengan menggunakan Scarb, Anda dapat menginstalnya dengan mengikuti petunjuk di bawah ini.
Langkah pertama adalah menginstal Cairo. Kami akan mengunduh Cairo secara manual, menggunakan repositori cairo atau dengan skrip instalasi. Anda akan memerlukan koneksi internet untuk mengunduhnya.
Persyaratan
Pertama-tama, Anda harus memiliki Rust dan Git terinstal.
# Instal Rust stabil
rustup override set stable && rustup update
Instal Git.
Memasang Cairo dengan Skrip (Installer oleh Fran)
Instal
Jika Anda ingin menginstal rilis tertentu dari Cairo daripada yang terbaru, atur variabel lingkungan CAIRO_GIT_TAG
(misalnya export CAIRO_GIT_TAG=v2.2.0
).
curl -L https://github.com/franalgaba/cairo-installer/raw/main/bin/cairo-installer | bash
Setelah menginstal, ikuti instruksi ini untuk menyiapkan lingkungan shell Anda.
Perbarui
rm -fr ~/.cairo
curl -L https://github.com/franalgaba/cairo-installer/raw/main/bin/cairo-installer | bash
Hapus Instalasi
Cairo terinstal di dalam $CAIRO_ROOT
(default: ~/.cairo). Untuk menghapusnya, cukup hapus:
rm -fr ~/.cairo
kemudian hapus tiga baris ini dari .bashrc:
export PATH="$HOME/.cairo/target/release:$PATH"
dan terakhir, restart shell Anda:
exec $SHELL
Menyiapkan Lingkungan Shell Anda untuk Cairo
- Tetapkan variabel lingkungan
CAIRO_ROOT
untuk menunjukkan path tempat Cairo akan menyimpan datanya.$HOME/.cairo
adalah defaultnya. Jika Anda menginstal Cairo melalui Git checkout, kami sarankan untuk menetapkannya ke lokasi yang sama dengan tempat Anda mengclonenya. - Tambahkan
cairo-*
eksekutor kePATH
Anda jika belum ada di sana
Pengaturan di bawah ini seharusnya berfungsi untuk sebagian besar pengguna untuk penggunaan umum.
-
Untuk bash:
File startup Bash bervariasi secara luas antara distribusi dalam hal yang mana di antara mereka mengambil sumber daya, dalam keadaan apa, dalam urutan apa, dan konfigurasi tambahan apa yang mereka lakukan. Oleh karena itu, cara yang paling dapat diandalkan untuk mendapatkan Cairo di semua lingkungan adalah dengan menambahkan perintah konfigurasi Cairo ke kedua
.bashrc
(untuk shell interaktif) dan file profil yang akan digunakan Bash (untuk shell login).Pertama, tambahkan perintah ke
~/.bashrc
dengan menjalankan perintah berikut di terminal Anda:echo 'export CAIRO_ROOT="$HOME/.cairo"' >> ~/.bashrc echo 'command -v cairo-compile >/dev/null || export PATH="$CAIRO_ROOT/target/release:$PATH"' >> ~/.bashrc
Kemudian, jika Anda memiliki
~/.profile
,~/.bash_profile
, atau~/.bash_login
, tambahkan perintah di sana juga. Jika Anda tidak memiliki yang ini, tambahkan ke~/.profile
.-
untuk menambahkan ke
~/.profile
:echo 'export CAIRO_ROOT="$HOME/.cairo"' >> ~/.profile echo 'command -v cairo-compile >/dev/null || export PATH="$CAIRO_ROOT/target/release:$PATH"' >> ~/.profile
-
untuk menambahkan ke
~/.bash_profile
:echo 'export CAIRO_ROOT="$HOME/.cairo"' >> ~/.bash_profile echo 'command -v cairo-compile >/dev/null || export PATH="$CAIRO_ROOT/target/release:$PATH"' >> ~/.bash_profile
-
-
Untuk Zsh:
echo 'export CAIRO_ROOT="$HOME/.cairo"' >> ~/.zshrc echo 'command -v cairo-compile >/dev/null || export PATH="$CAIRO_ROOT/target/release:$PATH"' >> ~/.zshrc
Jika Anda ingin mendapatkan Cairo di shell login non-interaktif juga, tambahkan perintah ke
~/.zprofile
atau~/.zlogin
. -
Untuk Fish shell:
Jika Anda memiliki Fish 3.2.0 atau yang lebih baru, jalankan ini secara interaktif:
set -Ux CAIRO_ROOT $HOME/.cairo fish_add_path $CAIRO_ROOT/target/release
Atau, jalankan potongan kode di bawah ini:
set -Ux CAIRO_ROOT $HOME/.cairo set -U fish_user_paths $CAIRO_ROOT/target/release $fish_user_paths
Di MacOS, Anda mungkin juga ingin menginstal Fig yang menyediakan penyelesaian shell alternatif untuk banyak alat baris perintah dengan antarmuka popup mirip IDE di jendela terminal. (Catat bahwa penyelesaian mereka independen dari basis kode Cairo jadi mereka mungkin sedikit tidak sinkron untuk perubahan antarmuka yang sangat baru.)
Restart shell Anda
agar perubahan pada PATH
berlaku.
exec "$SHELL"
Menginstal Cairo Secara Manual (Panduan oleh Abdel)
Langkah 1: Instalasi Cairo 1.0
Jika Anda menggunakan sistem Linux x86 dan dapat menggunakan biner rilis, unduh Cairo di sini: https://github.com/starkware-libs/cairo/releases.
Untuk yang lain, kami sarankan untuk mengompilasi Cairo dari sumber sebagai berikut:
# Mulai dengan menetapkan variabel lingkungan CAIRO_ROOT
export CAIRO_ROOT="${HOME}/.cairo"
# Buat folder .cairo jika belum ada
mkdir $CAIRO_ROOT
# clone compiler Cairo ke $CAIRO_ROOT (root default)
cd $CAIRO_ROOT && git clone git@github.com:starkware-libs/cairo.git .
# OPSIONAL/DIANJURKAN: Jika Anda ingin menginstal versi tertentu dari compiler
# Dapatkan semua tag (versi)
git fetch --all --tags
# Lihat tag (Anda juga bisa melakukan ini di repositori compiler cairo)
git describe --tags `git rev-list --tags`
# Pilih versi yang Anda inginkan
git checkout tags/v2.2.0
# Hasilkan biner rilis
cargo build --all --release
.
CATATAN: Menjaga Cairo tetap terbaru
Sekarang bahwa compiler Cairo Anda berada di repositori yang di-clone, yang perlu Anda lakukan hanya menarik perubahan terbaru dan membangun kembali sebagai berikut:
cd $CAIRO_ROOT && git fetch && git pull && cargo build --all --release
Langkah 2: Tambahkan eksekutor Cairo 1.0 ke path Anda
export PATH="$CAIRO_ROOT/target/release:$PATH"
CATATAN: Jika menginstal dari biner Linux, sesuaikan path tujuan.
Langkah 3: Menyiapkan Server Bahasa
Ekstensi VS Code
- Jika Anda memiliki ekstensi Cairo 0 sebelumnya terinstal, Anda dapat menonaktifkannya/menghapusnya.
- Instal ekstensi Cairo 1 untuk penyorotan sintaks yang tepat dan navigasi kode. Anda dapat menemukan tautan ke ekstensi di sini, atau cukup cari "Cairo 1.0" di pasar VS Code.
- Ekstensi akan langsung berfungsi setelah Anda menginstal Scarb.
Cairo Language Server tanpa Scarb
Jika Anda tidak ingin bergantung pada Scarb, Anda masih dapat menggunakan Cairo Language Server dengan biner compiler.
Dari Langkah 1, biner cairo-language-server
harus dibangun dan menjalankan perintah ini akan menyalin jalurnya ke clipboard Anda.
which cairo-language-server | pbcopy
Perbarui cairo1.languageServerPath
dari ekstensi Cairo 1.0 dengan menempelkan path tersebut.