Block pada Crystal hidup selama pemanggilan metode yang menerimanya, dan secara default tidak ada nama yang merujuknya di dalam metode tersebut. Topik ini membahas parameter ampersand yang menangkap block, yaitu potongan kode yang dilampirkan pada pemanggilan metode dan dijalankan oleh metode itu, sehingga block dapat dipanggil ulang, disimpan, dan diserahkan kembali seperti nilai lain. Bahasan mencakup anotasi tipe pada parameter block, penyerahan Proc yang sudah ada, serta sintaks singkat &.metode untuk block yang isinya satu pemanggilan.
Parameter ampersand menangkap block#
Metode yang menerima block tanpa penangkapan hanya dapat menjalankannya melalui yield pada saat pemanggilan berlangsung. Begitu metode selesai, block tersebut tidak lagi dapat dijangkau karena tidak ada nama yang menyimpannya. Parameter berawalan ampersand pada definisi metode mengubah keadaan ini. Compiler menangkap block yang dilampirkan pada pemanggilan, mengemasnya menjadi Proc, dan menyediakannya di dalam metode sebagai parameter biasa.
Program berikut mendefinisikan metode eksekusi dengan parameter &blok yang diberi anotasi tipe Int32 -> Int32. Di dalam metode, blok.call(5) menjalankan block yang tertangkap dengan argumen 5. Dua pemanggilan memasok block yang berbeda, sehingga hasilnya pun berbeda.
# parameter &blok menangkap block menjadi Proc
def eksekusi(&blok : Int32 -> Int32)
blok.call(5)
end
puts eksekusi { |n| n * n }
puts eksekusi { |n| n + 100 }# jalankan program penangkapan block
crystal run penangkapan.cr25
105Angka 25 berasal dari block yang mengalikan argumen dengan dirinya sendiri, sedangkan angka 105 berasal dari block yang menambahkan 100. Parameter &blok tidak menentukan isi block, melainkan hanya menyediakan nama bagi block yang dikirim pemanggil. Isi block sepenuhnya ditulis pada titik pemanggilan, sehingga satu metode dapat melayani banyak perilaku tanpa perubahan apa pun di dalam metodenya.
Anotasi tipe pada parameter block#
Anotasi tipe pada parameter block dapat dihilangkan, tetapi keputusan ini berdampak nyata pada apa yang dapat dilakukan terhadap block tersebut. Program berikut menangkap block tanpa anotasi apa pun, lalu menanyakan tipenya kepada compiler.
def jalankan(&blok)
blok
end
tanda = jalankan { "siap" }
puts tanda.class# periksa tipe block yang tertangkap tanpa anotasi
crystal run tanpa-anotasi.crProc(Nil)Tipe Proc(Nil) menyatakan Proc tanpa parameter yang menghasilkan Nil. Penangkapan tanpa anotasi tidak menyisakan informasi tentang parameter maupun nilai balik block, sehingga compiler menilai keduanya secara paling konservatif. Block yang menerima parameter tidak dapat dipanggil dari penangkapan semacam ini, dan nilai yang dihasilkan block menghilang menjadi Nil.
Anotasi tipe membalik keadaan tersebut. Dengan anotasi Int32 -> Int32, compiler menilai parameter block sebagai Int32 dan memeriksa isi block terhadap tipe itu. Program berikut melampirkan block yang memanggil upcase pada parameternya, padahal anotasi metode menuntut block bertipe Int32 -> Int32.
def eksekusi(&blok : Int32 -> Int32)
blok.call(5)
end
eksekusi { |teks| teks.upcase }Showing last frame. Use --error-trace for full trace.
In tipe-salah.cr:5:24
5 | eksekusi { |teks| teks.upcase }
^-----
Error: undefined method 'upcase' for Int32Pesan error menunjuk langsung ke teks.upcase karena compiler menilai teks sebagai Int32 sesuai anotasi, sedangkan upcase bukan metode milik Int32. Pemeriksaan ini bekerja pada saat kompilasi, sehingga ketidakcocokan antara anotasi dan isi block tertangkap sebelum program berjalan. Inilah yang membuat anotasi tipe informatif meskipun sifatnya opsional: anotasi mendokumentasikan kontrak block sekaligus memberi compiler dasar untuk memeriksanya.
Block tertangkap dapat disimpan#
Hasil penangkapan adalah nilai biasa bertipe Proc, sehingga block yang tertangkap dapat disimpan ke variabel, dikumpulkan dalam array, dan dipanggil berkali-kali. Program berikut memanfaatkan sifat ini. Metode pilih tidak memanggil block-nya sama sekali, melainkan mengembalikan block yang tertangkap kepada pemanggil.
def pilih(&blok : Int32 -> Int32)
blok
end
# block yang tertangkap dapat disimpan seperti nilai lainnya
operasi = pilih { |n| n * 10 }
puts operasi.call(4)
puts operasi.class
arsip = [pilih { |n| n + 1 }, pilih { |n| n - 1 }]
puts arsip.map &.call(10)
# Proc yang sudah ada dapat diserahkan ke parameter block
def eksekusi(&blok : Int32 -> Int32)
blok.call(5)
end
pangkat = ->(n : Int32) { n * n }
puts eksekusi(&pangkat)# jalankan program penyimpanan block
crystal run penyimpanan.cr40
Proc(Int32, Int32)
[11, 9]
25Baris pertama keluaran berasal dari operasi.call(4), sedangkan baris kedua mengonfirmasi tipe operasi sebagai Proc(Int32, Int32). Variabel arsip menampung dua Proc sekaligus, dan map &.call(10) memanggil keduanya dengan argumen yang sama. Bagian terakhir memperlihatkan arah sebaliknya: Proc pangkat yang dibuat dengan literal -> diserahkan ke parameter block melalui eksekusi(&pangkat), sehingga block yang dibutuhkan metode tidak harus ditulis langsung pada pemanggilan.
Sintaks singkat &.metode#
Banyak block hanya memanggil satu metode pada parameternya. Untuk pola ini Crystal menyediakan sintaks singkat &.metode, yaitu block anonim yang meneruskan parameternya ke metode yang disebut. Baris buah.map &.size pada program berikut setara dengan buah.map { |b| b.size }, tetapi tanpa parameter yang perlu diberi nama.
buah = ["apel", "durian"]
puts buah.map &.size
puts buah.map &.upcase
# sintaks singkat dipakai bersama metode generik buatan sendiri
def ubah_semua(nilai : Array(T), &operasi : T -> U) forall T, U
hasil = [] of U
nilai.each do |item|
hasil << operasi.call(item)
end
hasil
end
puts ubah_semua(buah, &.size)
puts ubah_semua([3, 1, 2], &.even?)# jalankan program sintaks singkat
crystal run sintaks-singkat.cr[4, 6]
["APEL", "DURIAN"]
[4, 6]
[false, false, true]Dua baris pertama memetakan array string ke panjangnya dan ke huruf kapitalnya. Metode ubah_semua pada program yang sama adalah metode generik buatan sendiri dengan parameter &operasi : T -> U disertai forall T, U. Bentuk ini menerima block yang mengubah elemen bertipe T menjadi nilai bertipe U, dan sintaks singkatnya sama persis: ubah_semua(buah, &.size) memetakan string ke Int32, sedangkan ubah_semua([3, 1, 2], &.even?) memetakan bilangan ke Bool. Tipe U tersimpulkan dari block pada tiap pemanggilan, sehingga satu metode melayani banyak tipe hasil.
Kesalahan umum#
Dua kesalahan berikut sering muncul ketika block mulai ditangkap dan diserahkan. Keduanya ditolak compiler pada saat kompilasi, sehingga tidak pernah menjadi perilaku diam-diam saat program berjalan.
Kesalahan pertama adalah memanggil block berparameter dari penangkapan yang tidak diberi anotasi tipe. Compiler menilai block tertangkap sebagai Proc(Nil), sehingga pemanggilan dengan satu argumen tidak cocok dengan tipe yang tersedia.
def jalankan(&blok)
blok.call(5)
end
puts jalankan { |n| n * n }Showing last frame. Use --error-trace for full trace.
In tanpa-tipe.cr:5:6
5 | puts jalankan { |n| n * n }
^-------
Error: wrong number of block parameters (given 1, expected 0)Perbaikannya adalah mencantumkan anotasi tipe pada parameter, misalnya def jalankan(&blok : Int32 -> Int32), sehingga block tertangkap membawa informasi parameter dan nilai baliknya.
Kesalahan kedua adalah menyerahkan Proc tanpa ampersand. Tanpa &, compiler menilai pangkat sebagai argumen posisi biasa, sedangkan eksekusi tidak memiliki parameter posisi sama sekali dan hanya menerima block.
def eksekusi(&blok : Int32 -> Int32)
blok.call(5)
end
pangkat = ->(n : Int32) { n * n }
puts eksekusi(pangkat)Showing last frame. Use --error-trace for full trace.
In tanpa-ampersand.cr:6:6
6 | puts eksekusi(pangkat)
^-------
Error: 'eksekusi' is expected to be invoked with a block, but no block was givenMenambahkan ampersand pada pemanggilan, yaitu eksekusi(&pangkat), menyelesaikan kesalahan ini karena Proc diserahkan sebagai block.
Ringkasan#
- Parameter
&blokmenangkap block pada saat pemanggilan dan menyediakannya di dalam metode sebagaiProcyang dapat dipanggil denganblok.callserta disimpan seperti nilai lain. - Anotasi tipe seperti
&blok : Int32 -> Int32bersifat opsional tetapi informatif: compiler memeriksa isi block terhadap anotasinya, sedangkan penangkapan tanpa anotasi dinilai sebagaiProc(Nil)yang tidak dapat dipanggil dengan parameter. - Proc yang sudah ada dapat diserahkan ke parameter block dengan menuliskan
&sebelum namanya pada pemanggilan. - Sintaks
&.metodeadalah bentuk singkat untuk block yang isinya satu pemanggilan metode pada parameternya, dan sah pula untuk metode generik.
Referensi#
- The Crystal Programming Language Reference — crystal-lang.org — Dokumentasi resmi bahasa yang memuat bab blocks and procs tentang penangkapan block dengan parameter ampersand.
- Dokumentasi API Crystal 1.22.1 — crystal-lang.org
— Rujukan class
Procbeserta metodecallyang digunakan untuk menjalankan block yang tertangkap.