Block dan yield

Block sebagai bagian pemanggilan metode dan yield sebagai titik pemanggilannya.

Daftar isi

Block adalah potongan kode yang dilekatkan pada satu pemanggilan metode dan dijalankan oleh metode tersebut, bukan objek yang berdiri sendiri. Topik ini membahas cara menuliskan block pada pemanggilan metode, cara yield menjadikannya titik pemanggilan, penerusan argumen ke block, nilai balik block, pemanggilan yang berulang, serta penegakan aturan block oleh compiler. Setelah membaca topik ini, metode yang menerima block dapat dibaca dan ditulis dengan mengetahui kontrak yang mengikat kedua sisinya.

Block ditulis langsung setelah argumen pemanggilan metode, baik dengan do ... end maupun dengan kurung kurawal { ... }. Block tidak memiliki nama dan tidak dapat dipanggil dari tempat lain karena masa hidupnya melekat pada satu pemanggilan metode itu saja. Di dalam tubuh metode, kata kunci yield menandai titik pemanggilan block. Metode berikut tidak memiliki parameter apa pun, tetapi tubuhnya memanggil block dua kali.

crystal dua_kali.cr
# yield memanggil block dua kali

def dua_kali
  yield
  yield
end

dua_kali do
  puts "Piket pagi"
end

Jalankan berkasnya dengan perintah berikut.

bash
# jalankan program dua_kali.cr
crystal run dua_kali.cr
text Keluaran
Piket pagi
Piket pagi

Definisi dua_kali tidak memuat parameter, sedangkan pemanggilannya membawa block. Compiler menerima pemanggilan ini karena kehadiran yield di dalam tubuh metode menyatakan bahwa metode tersebut dirancang untuk dipanggil dengan block. Baris puts "Piket pagi" berada di dalam block, sehingga setiap yield menjalankannya satu kali dan keluarannya muncul dua kali. Urutan keluaran mengikuti urutan yield, bukan urutan penulisan block. Pemanggilan yield tanpa argumen digunakan pada contoh ini karena blocknya tidak membutuhkan nilai apa pun.

yield sebagai titik pemanggilan#

yield bukan sekadar penanda, melainkan ekspresi yang dievaluasi ketika metode berjalan. Bentuk yield(nilai) meneruskan argumen ke parameter block, yaitu variabel yang ditulis di antara dua garis tegak seperti |n|, dan nilai ekspresi terakhir di tubuh block menjadi nilai dari ekspresi yield itu sendiri. Dengan dua arah ini, metode mengirim data ke block dan menerima hasil kembali dari block.

crystal nilai_block.cr
# yield(nilai) meneruskan argumen dan menampung nilai balik block

def catat(nilai)
  hasil = yield(nilai)
  puts "Block mengembalikan: #{hasil}"
end

catat(24) do |n|
  puts "Nilai diterima: #{n}"
  n * 2
end
text Keluaran
Nilai diterima: 24
Block mengembalikan: 48

Pemanggilan catat(24) menjalankan metode dengan parameter nilai, lalu yield(nilai) meneruskan nilai itu ke parameter block n. Block menampilkan nilai yang diterimanya, kemudian menghitung n * 2 sebagai ekspresi terakhirnya. Nilai 48 itulah yang ditampung variabel hasil di dalam metode. Nilai balik block tidak wajib ditampung, tetapi menampungnya membuat kontrak dua arah itu terlihat pada kedua sisinya. Kontrak antara metode dan block dengan demikian jelas, yaitu yield(nilai) menyuplai daftar parameter block, sedangkan ekspresi terakhir block menyuplai nilai yield.

Mengulang pemanggilan block#

Jumlah pemanggilan block ditentukan oleh alur metode, bukan oleh block itu sendiri. yield boleh ditulis di dalam percabangan maupun perulangan, sehingga satu block dapat dijalankan berkali-kali dengan nilai yang berbeda-beda. Metode berikut menjalankan block untuk setiap elemen array dan meneruskan dua argumen sekaligus, yaitu elemennya dan nomor urutnya.

crystal tiap_hari.cr
# yield berulang dengan argumen yang berbeda setiap pemanggilan

def tiap_hari
  hari = ["Senin", "Selasa", "Rabu"]
  i = 0
  while i < hari.size
    yield hari[i], i + 1
    i += 1
  end
end

tiap_hari do |nama, nomor|
  puts "Hari ke-#{nomor}: #{nama}"
end
text Keluaran
Hari ke-1: Senin
Hari ke-2: Selasa
Hari ke-3: Rabu

Perulangan while di tubuh tiap_hari berjalan tiga kali, dan setiap iterasi memanggil yield hari[i], i + 1. Dua argumen itu diterima parameter block nama dan nomor, sehingga block dapat memformat keduanya sekaligus. Nomor urut mulai dari satu karena i + 1 dihitung di sisi metode sebelum diteruskan, sehingga block tidak perlu menghitungnya sendiri. Pola inilah yang mendasari metode perulangan pada array seperti each dan map, yang seluruhnya mengandalkan yield di dalam tubuhnya. Dengan menulis metode seperti ini sendiri, pembagian tugas menjadi jelas, yaitu metode mengatur kapan dan dengan nilai apa block dipanggil, sedangkan block menentukan apa yang dikerjakan pada setiap pemanggilan.

Compiler menegakkan kontrak block#

Crystal memeriksa kecocokan block pada saat kompilasi. Metode yang mengandung yield wajib dipanggil dengan block, dan pemanggilan tanpa block ditolak compiler sebelum program sempat berjalan.

crystal tanpa_block.cr
# Program ini ditolak compiler

def panggil
  yield
end

panggil

Jalankan berkasnya dengan perintah berikut, lalu amati pesan errornya.

bash
# jalankan program tanpa_block.cr
crystal run tanpa_block.cr
text Keluaran
Showing last frame. Use --error-trace for full trace.

In tanpa_block.cr:7:1

 7 | panggil
     ^------
Error: 'panggil' is expected to be invoked with a block, but no block was given

Pesan error menunjuk ke pemanggilan panggil, bukan ke yield di dalam tubuhnya, karena block yang hilang terdeteksi pada situs pemanggilan. Compiler mengetahui bahwa panggil mengandung yield, sehingga setiap pemanggilannya harus menyertakan block. Kekurangan block dengan demikian tidak pernah menjadi error runtime, melainkan berhenti di tahap kompilasi.

Kecocokan yang sebaliknya juga diperiksa. Metode yang tidak mengandung yield menolak block yang diberikan kepadanya.

crystal block_diabaikan.cr
# Program ini ditolak compiler

def tanpa_yield
  puts "Di dalam metode"
end

tanpa_yield do
  puts "Baris ini tidak pernah dijalankan"
end
bash
# jalankan program block_diabaikan.cr
crystal run block_diabaikan.cr
text Keluaran
Showing last frame. Use --error-trace for full trace.

In block_diabaikan.cr:7:1

 7 | tanpa_yield do
     ^----------
Error: 'tanpa_yield' is not expected to be invoked with a block, but a block was given

Metode tanpa_yield tidak mengandung yield, sehingga block yang melekat pada pemanggilannya tidak memiliki titik pemanggilan. Alih-alih mengabaikan block itu secara diam-diam, compiler menolak programnya dan menandai baris pemanggilan. Penegakan dua arah ini membuat kontrak block terdokumentasi pada definisi metodenya, yaitu kehadiran yield menyatakan block wajib, sedangkan ketiadaannya menyatakan block tidak diterima.

Pemilihan antara do … end dan kurung kurawal#

Kedua bentuk penulisan block sama-sama diterima pada semua pemanggilan metode, sehingga pemilihannya bebas dari sisi compiler. Yang membedakan adalah konvensi keterbacaan di antara pengembang dan ikatan masing-masing bentuk pada ekspresi berantai. Keduanya dibahas berurutan pada bagian ini.

Kedua bentuk tidak persis setara dalam satu hal, yaitu ikatannya pada ekspresi berantai. Kurung kurawal melekat pada pemanggilan terkanan pada rantai ekspresi, sehingga pada baris berikut block menempel pada map dan perhitungan berjalan sesuai harapan.

crystal kurung_kurawal.cr
# kurung kurawal menempel pada pemanggilan map

angka = [1, 2, 3]

p angka.map { |x| x * 2 }
text Keluaran
[2, 4, 6]

Bentuk do ... end memiliki ikatan yang lebih longgar dan melekat pada pemanggilan terkiri pada rantai tersebut. Pada ekspresi p angka.map do ... end, block melekat pada p, bukan pada map.

crystal do_end.cr
# ikatan do ... end berbeda dari kurung kurawal

angka = [1, 2, 3]

p angka.map do |x|
  x * 2
end
bash
# jalankan program do_end.cr
crystal run do_end.cr
text Keluaran
Showing last frame. Use --error-trace for full trace.

In do_end.cr:5:9

 5 | p angka.map do |x|
             ^--
Error: 'Array(Int32)#map' is expected to be invoked with a block, but no block was given

Karena block menempel pada p, pemanggilan map berjalan tanpa block dan compiler menolaknya dengan pesan yang menyebut Array(Int32)#map. Kekeliruan ikatan semacam ini mudah terlewat karena sintaks blocknya sendiri tidak salah, tetapi compiler Crystal menangkapnya pada saat kompilasi. Apabila tubuh block memang panjang, tulis pemanggilannya tanpa metode pembungkus di depannya, atau tampung hasilnya ke variabel terlebih dahulu sebelum ditampilkan.

Ringkasan#

  • Block ditulis melekat pada satu pemanggilan metode dengan do ... end atau kurung kurawal, dan yield di dalam tubuh metode adalah titik pemanggilannya.
  • yield(nilai) meneruskan argumen ke parameter block, sedangkan ekspresi terakhir block menjadi nilai dari ekspresi yield.
  • Jumlah dan waktu pemanggilan block sepenuhnya ditentukan oleh alur metode yang memanggilnya.
  • Compiler menegakkan kontrak block secara dua arah, yaitu metode dengan yield wajib dipanggil dengan block dan metode tanpa yield menolak block.

Referensi#