Proc dan lambda

Proc dan lambda sebagai callable kelas satu beserta perbedaan semantik return-nya.

Daftar isi

Proc adalah callable, yaitu objek yang membungkus potongan kode sehingga kode tersebut dapat disimpan, diteruskan, dan dipanggil seperti nilai lainnya. Topik ini membahas penulisan literal Proc, tipenya, pemanggilan dengan metode call, penggunaannya sebagai argumen metode dan elemen array, semantik return di dalam Proc dibandingkan di dalam block, serta pemetaan istilah dari Ruby. Setelah membaca topik ini, kode yang dipindah-pindah sebagai data dapat ditulis sambil tahu kapan return mengembalikan nilai dan ke mana alurnya berakhir.

Proc sebagai objek callable#

Literal Proc ditulis dengan tanda panah ->, diikuti daftar parameter bertipe, lalu tubuh kodenya. Tipe hasilnya adalah Proc dengan daftar tipe parameternya, misalnya Proc(Int32, Int32) yang menyatakan satu parameter Int32 dan satu nilai balik Int32. Berbeda dari block, Proc adalah objek yang berdiri sendiri, sehingga dapat ditampung variabel dan dipanggil berulang kali dengan metode call.

crystal proc_sederhana.cr
# literal Proc, tipenya, dan pemanggilan dengan call

dua_kali = ->(x : Int32) { x * 2 }

puts typeof(dua_kali)
puts dua_kali.call(21)

Jalankan berkasnya dengan perintah berikut.

bash
# jalankan program proc_sederhana.cr
crystal run proc_sederhana.cr
text Keluaran
Proc(Int32, Int32)
42

Metode typeof menampilkan tipe objeknya, yaitu Proc(Int32, Int32), dan call(21) menjalankan tubuh Proc dengan argumen 21. Tipe tersebut membuat compiler mengetahui tipe argumen dan tipe nilai balik setiap Proc sejak saat kompilasi, sehingga pemanggilan call diperiksa kecocokannya seperti pemanggilan metode biasa. Penulisan tipe pada parameter literal, yaitu x : Int32, menjadi bagian dari tipe Proc yang terbentuk.

Proc berpindah sebagai data#

Karena berupa objek, Proc dapat dipindahkan seperti data lainnya, yaitu ditampung variabel, diteruskan sebagai argumen metode, dan dikumpulkan di dalam array. Parameter metode yang menerima Proc dianotasikan dengan tipenya secara lengkap. Contoh berikut mempraktikkan ketiga bentuk perpindahan itu dalam satu program.

crystal proc_berpindah.cr
# Proc sebagai argumen metode dan sebagai elemen array

def terapkan(nilai : Int32, operasi : Proc(Int32, Int32))
  operasi.call(nilai)
end

tambah = ->(x : Int32) { x + 10 }
kalikan = ->(x : Int32) { x * 2 }

daftar = [tambah, kalikan]

puts terapkan(5, tambah)
puts terapkan(5, kalikan)

daftar.each do |operasi|
  puts operasi.call(7)
end
text Keluaran
15
10
17
14

Metode terapkan menerima parameter operasi bertipe Proc(Int32, Int32) dan memanggilnya tanpa mengetahui isi kodenya. Pemanggilan terapkan(5, tambah) menghasilkan 15, sedangkan terapkan(5, kalikan) menghasilkan 10, karena perilaku yang dijalankan ditentukan oleh Proc yang diteruskan. Array daftar menampung dua Proc dan tipenya menjadi Array(Proc(Int32, Int32)), sehingga setiap elemen yang keluar dari perulangan tetap dapat dipanggil dengan call. Perilaku sebagai data semacam ini adalah ranah Proc, sedangkan block dirancang untuk melekat pada satu pemanggilan metode.

next pada block dan penolakannya di dalam Proc#

Di dalam block, kata kunci next mengakhiri satu pemanggilan block lebih awal dan menetapkan nilai kembaliannya untuk iterasi itu. Mekanisme ini lazim digunakan untuk melewati kasus tertentu tanpa menyarangkan seluruh tubuh block ke dalam percabangan. Contoh berikut menggunakan next pada satu elemen array.

crystal next_di_block.cr
# next mengembalikan nilai dari block

hasil = [1, 2, 3].map do |x|
  next 0 if x == 2
  x * 10
end

p hasil
text Keluaran
[10, 0, 30]

Pemanggilan next 0 pada elemen 2 mengakhiri block lebih awal dan menjadikan 0 sebagai hasil iterasi tersebut, sedangkan elemen lain mengikuti ekspresi terakhirnya. Kebiasaan ini sering terbawa ketika kode dipindahkan dari block ke Proc, dan hasilnya ditolak compiler.

crystal next_di_proc.cr
# Program ini ditolak compiler

kuadrat_aman = ->(x : Int32) do
  next 0 if x < 0
  x * x
end

puts kuadrat_aman.call(-3)
puts kuadrat_aman.call(4)

Jalankan berkasnya dengan perintah berikut, lalu amati pesan errornya.

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

In next_di_proc.cr:4:3

 4 | next 0 if x < 0
     ^
Error: invalid next

Pesan invalid next menegaskan bahwa next tidak dikenal di dalam literal Proc. Compiler tidak mengalihkan maknanya ke pemisah alur lain, sehingga pemindahan kode dari block ke Proc yang mengandung next wajib disunting. Pemisah alur yang sah untuk Proc dibahas pada bagian berikutnya.

Keluar awal dengan return di dalam Proc#

Kata kunci return di dalam literal Proc mengembalikan nilai dari Proc itu sendiri, bukan dari metode yang melingkupinya. Pemanggilan call berakhir pada titik return, dan alur program yang memanggilnya berlanjut seperti biasa. Nilai yang dinyatakan setelah return menjadi nilai kembalian call tersebut.

crystal proc_return.cr
# return di dalam Proc mengembalikan nilai dari Proc itu sendiri

kuadrat_aman = ->(x : Int32) do
  if x < 0
    return 0
  end
  x * x
end

puts kuadrat_aman.call(-3)
puts kuadrat_aman.call(4)
text Keluaran
0
16

Pemanggilan kuadrat_aman.call(-3) masuk ke cabang if dan berakhir pada return 0, sedangkan kuadrat_aman.call(4) melewati cabang itu dan mengembalikan hasil x * x. Kedua nilai tersebut keluar melalui call, dan baris puts sesudahnya tetap dijalankan, yang berarti kode yang melingkupi tidak terpengaruh oleh return di dalam Proc. Perilaku ini menjadikan return pengganti yang tepat untuk next pada Proc.

return pada block mengakhiri metode#

Makna return berbeda ketika ia ditulis di dalam block. Di sana return mengakhiri metode yang melingkupi block, bukan block itu sendiri, dan nilai kembaliannya menjadi nilai metode tersebut. Contoh berikut membandingkan pemanggilan yang berakhir di tengah perulangan dengan pemanggilan yang berjalan sampai selesai.

crystal return_di_block.cr
# return di dalam block mengakhiri metode yang melingkupinya

def cari_pertama_genap(daftar)
  daftar.each do |x|
    return x if x % 2 == 0
  end
  -1
end

p cari_pertama_genap([7, 3, 8, 10])
p cari_pertama_genap([7, 3, 5])
text Keluaran
8
-1

Pada daftar pertama, return x dieksekusi ketika x bernilai 8, sehingga metode cari_pertama_genap langsung berakhir dengan nilai 8 tanpa menunggu sisa iterasi. Pada daftar kedua tidak ada elemen genap, sehingga perulangan selesai secara normal dan ekspresi terakhir metode, yaitu -1, menjadi nilainya. Satu kata kunci yang sama dengan demikian memiliki dua batas keluar yang berbeda, tergantung pada apakah ia ditulis di dalam Proc atau di dalam block.

Pemetaan istilah dari Ruby#

Pada Ruby, potongan kode yang dapat dipanggil tersedia dalam beberapa bentuk, yaitu block, objek Proc, dan lambda. Crystal hanya menyediakan satu objek callable, yaitu Proc. Kata kunci lambda tidak ada, dan pemanggilan lambda ditolak compiler.

crystal lambda_keyword.cr
# Program ini ditolak compiler

f = lambda { |x| x }

puts f.call(1)

Jalankan berkasnya dengan perintah berikut, lalu amati pesan errornya.

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

In lambda_keyword.cr:3:5

 3 | f = lambda { |x| x }
         ^-----
Error: undefined local variable or method 'lambda' for top-level

Pesan error mengonfirmasi bahwa lambda tidak dikenal sebagai variabel maupun metode pada tingkat atas program. Pemetaan antara kedua bahasa dapat dirangkum sebagai berikut.

Konsep di RubyPadanan di CrystalCatatan perilaku
Block (do ... end atau { ... })BlockMelekat pada satu pemanggilan metode, next sah di dalamnya, dan return mengakhiri metode yang melingkupinya.
Objek Proc dan lambdaLiteral Proc ->Objek callable bertipe tetap yang dipanggil dengan call, dan return mengembalikan nilai dari Proc itu sendiri.
Kata kunci lambdaTidak tersediaCrystal hanya mengenal Proc sebagai objek callable, tanpa pembeda antara proc dan lambda.

Pada semantik return, Proc Crystal berperilaku seperti lambda Ruby, yaitu keluar dari callable-nya sendiri, sedangkan pola Ruby yang memanfaatkan return pada proc untuk keluar dari metode lingkupnya setara dengan menulis return di dalam block Crystal. Perbedaan penyebutan istilah tidak mengubah cara pemakaiannya sehari-hari, karena literal -> dan metode call sudah mencakup kebutuhan yang di Ruby dipecah ke proc dan lambda. Seluruh perilaku Crystal yang dibandingkan pada tabel di atas diuji langsung dengan Crystal 1.22.1, sehingga pemetaan ini dapat digunakan sebagai acuan saat memindahkan kode dari Ruby.

Ringkasan#

  • Literal ->(x : Int32) { ... } menghasilkan objek Proc bertipe eksplisit seperti Proc(Int32, Int32) yang dipanggil dengan metode call.
  • Proc dapat ditampung variabel, diteruskan sebagai argumen metode, dan dikumpulkan dalam array, sehingga perilaku dapat dipindahkan sebagai data.
  • return di dalam Proc mengembalikan nilai dari Proc itu sendiri, sedangkan return di dalam block mengakhiri metode yang melingkupinya.
  • next hanya sah di dalam block, dan kata kunci lambda dari Ruby tidak ada di Crystal, sehingga Proc menjadi satu-satunya objek callable.

Referensi#