Currying adalah teknik membentuk closure baru dari closure lama dengan membekukan sebagian argumennya, sehingga terbentuk fungsi parsial (partial application) yang parameternya sebagian sudah terisi. Topik ini membahas metode curry, rcurry, dan ncurry untuk membangun fungsi parsial tersebut, kemudian komposisi dengan operator << dan >> serta metode andThen untuk merangkai beberapa closure menjadi satu rantai pemanggilan. Setelah membaca topik ini, fungsi-fungsi kecil dapat dirakit menjadi varian baru tanpa menulis ulang isi closure asalnya.
Currying sebagai pembekuan parameter#
Metode curry membekukan satu atau beberapa argumen paling kiri milik sebuah closure dan mengembalikan closure baru yang hanya menunggu sisa argumennya. Closure asal tidak berubah, sehingga kedua bentuk dapat dipanggil secara berdampingan. Kemampuan ini melengkapi sifat closure sebagai objek, di samping properti owner dan delegate, karena metode curry juga milik kelas Closure.
Berkas berikut membekukan parameter pertama dari closure ulang yang semula memiliki dua parameter. Properti maximumNumberOfParameters memperlihatkan penurunan jumlah parameter pada closure hasil curry.
// curry: membekukan parameter paling kiri
def ulang = { int n, String teks -> teks * n }
def duaKali = ulang.curry(2)
println "Hasil baru : ${duaKali('hola')}"
println "Setara : ${ulang(2, 'hola')}"
println "Param baru : ${duaKali.maximumNumberOfParameters}"
println "Param asli : ${ulang.maximumNumberOfParameters}"Hasil baru : holahola
Setara : holahola
Param baru : 1
Param asli : 2Nilai 2 yang diberikan kepada curry tersimpan di dalam closure baru bernama duaKali, dan pemanggilan duaKali('hola') menghasilkan keluaran yang sama dengan ulang(2, 'hola'). Jumlah parameter closure baru turun dari dua menjadi satu, sedangkan closure ulang tetap menerima dua argumen sebagaimana bentuk asalnya. Closure hasil curry diperlakukan seperti closure biasa, sehingga dapat disimpan, dikirim sebagai argumen, atau dibekukan kembali.
rcurry dan ncurry untuk posisi lain#
Metode curry selalu membekukan argumen dari posisi paling kiri. Apabila parameter yang ingin dibekukan justru berada di posisi akhir, metode rcurry membekukannya dari kanan. Untuk posisi di tengah atau untuk membekukan beberapa nilai sekaligus mulai dari indeks tertentu, metode ncurry menerima indeks awal sebagai argumen pertamanya.
// rcurry membekukan parameter paling kanan, ncurry pada indeks tertentu
def bagi = { pembilang, penyebut -> pembilang / penyebut }
def setengah = bagi.rcurry(2)
println "rcurry : ${setengah(10)}"
def volume = { double l, double w, double h -> l * w * h }
def lebarTetap = volume.ncurry(1, 2d)
println "ncurry : ${lebarTetap(3d, 4d)}"
println "Setara : ${volume(3d, 2d, 4d)}"rcurry : 5
ncurry : 24.0
Setara : 24.0Pada contoh rcurry, nilai 2 membekukan parameter penyebut, sehingga pemanggilan setengah(10) cukup memberikan pembilangnya dan menghasilkan 5. Pada contoh ncurry, indeks 1 membekukan parameter w, sehingga dua argumen yang diberikan kemudian mengisi l dan h secara berurutan. Hasilnya sama dengan pemanggilan volume(3d, 2d, 4d) secara langsung.
| Metode | Posisi nilai yang dibekukan | Contoh pada topik ini |
|---|---|---|
curry | dari parameter paling kiri | ulang.curry(2) |
rcurry | dari parameter paling kanan | bagi.rcurry(2) |
ncurry | mulai indeks tertentu, dapat lebih dari satu nilai | volume.ncurry(1, 2d) |
Kemampuan ncurry membekukan lebih dari satu nilai digunakan untuk membangun keluarga fungsi konversi suhu dari satu closure umum. Kedua fungsi hasil curry berbagi logika yang sama dan hanya berbeda pada dua nilai yang dibekukannya.
// ncurry membekukan beberapa nilai sekaligus mulai dari indeks tertentu
def konversi = { double faktor, double offset, double nilai -> nilai * faktor + offset }
def keFahrenheit = konversi.ncurry(0, 1.8d, 32d)
println "100 C = ${keFahrenheit(100d)} F"
println "0 C = ${keFahrenheit(0d)} F"
def keKelvin = konversi.ncurry(0, 1d, 273.15d)
println "25 C = ${keKelvin(25d)} K"100 C = 212.0 F
0 C = 32.0 F
25 C = 298.15 KKomposisi closure#
Komposisi membentuk closure baru yang menjalankan dua closure secara berurutan, dengan keluaran closure pertama menjadi masukan bagi closure berikutnya. Operator << menjalankan operand kanannya lebih dahulu, sedangkan operator >> menjalankan operand kirinya lebih dahulu. Sejak Groovy 4, kelas Closure juga menyediakan metode andThen yang menjalankan closure penerimanya lebih dahulu, kemudian closure yang menjadi argumennya, sehingga urutan eksekusinya terbaca searah dengan penulisannya.
// Komposisi closure: <<, >>, dan andThen
def tambahDua = { it + 2 }
def kaliTiga = { it * 3 }
def kaliTigaLaluTambah = tambahDua << kaliTiga
println "<< : ${kaliTigaLaluTambah(3)}"
def tambahLaluKaliTiga = tambahDua >> kaliTiga
println ">> : ${tambahLaluKaliTiga(3)}"
def viaAndThen = tambahDua.andThen(kaliTiga)
println "andThen: ${viaAndThen(3)}"
println "Tipe : ${viaAndThen.getClass().simpleName}"<< : 11
>> : 15
andThen: 15
Tipe : ComposedClosurePada baris pertama, tambahDua << kaliTiga menghasilkan 11 karena 3 dikalikan tiga lebih dahulu menjadi 9, kemudian ditambah dua. Pada baris kedua, tambahDua >> kaliTiga menjalankan tambahDua lebih dahulu sehingga hasilnya 15, dan metode andThen menghasilkan nilai yang sama. Keluaran terakhir menunjukkan bahwa hasil komposisi tetap sebuah closure, yaitu ComposedClosure, sehingga dapat dirantai lagi, dicurry, atau dikirim sebagai argumen.
Rantai andThen dapat diperpanjang dengan menambah panggilan andThen berikutnya. Contoh berikut membangun pipeline pembersihan teks dari tiga closure satu parameter, kemudian membandingkannya dengan rantai yang urutannya dibalik.
// andThen dapat dirantai menjadi pipeline
def bersihkan = { it.trim() }
def kecilkan = { it.toLowerCase() }
def bungkus = { "[$it]" }
def proses = bersihkan.andThen(kecilkan).andThen(bungkus)
println proses(' Halo Groovy ')
def terbalik = bungkus.andThen(kecilkan)
println terbalik(' Halo Groovy ')[halo groovy]
[ halo groovy ]Pada rantai proses, teks diubah dengan bersihkan, kecilkan, kemudian bungkus, sehingga hasilnya [halo groovy]. Pada rantai terbalik, bungkus dijalankan lebih dahulu, sehingga spasi di kedua ujung masih ikut terbungkus dan huruf baru dikecilkan setelahnya. Kedua keluaran menegaskan bahwa andThen selalu menjalankan closure penerimanya lebih dahulu.
Kesalahan umum#
Kesalahan pertama adalah membaca operator << dari kiri ke kanan seolah-olah operand kirinya dijalankan lebih dahulu. Kesalahan ini tidak memunculkan pesan error karena komposisi tetap sah secara sintaks, tetapi urutan eksekusinya terbalik dari harapan. Nilai yang dihitung tetap keluar, sehingga arah yang terbalik hanya ketahuan bila hasilnya diperiksa.
// Arah komposisi: operand kanan pada << dijalankan lebih dahulu
def tambahDua = { it + 2 }
def kaliTiga = { it * 3 }
def campuran = tambahDua << kaliTiga
println "tambahDua << kaliTiga : ${campuran(3)}"
println "Pembacaan keliru : ${kaliTiga(tambahDua(3))}"
println "Pembacaan benar : ${tambahDua(kaliTiga(3))}"tambahDua << kaliTiga : 11
Pembacaan keliru : 15
Pembacaan benar : 11Nilai campuran(3) sama dengan tambahDua(kaliTiga(3)), yaitu 11, dan bukan 15 yang dihasilkan urutan terbaliknya. Perbaikannya adalah membaca << sebagai operasi yang dimulai dari operand kanan, atau beralih ke andThen apabila urutan eksekusi ingin dibaca searah.
Kesalahan kedua adalah memberikan argumen yang sudah dibekukan kepada closure hasil curry. Groovy memeriksa jumlah argumen terhadap daftar parameter closure baru dan melempar MissingMethodException apabila tidak ada bentuk pemanggilan yang cocok.
// Closure hasil curry hanya menerima sisa argumennya
def ulang = { int n, String teks -> teks * n }
def duaKali = ulang.curry(2)
try {
duaKali(2, 'hola')
} catch (MissingMethodException e) {
println "Gagal: ${e.getClass().simpleName}"
}
println "Benar: ${duaKali('hola')}"Gagal: MissingMethodException
Benar: holaholaPemanggilan yang benar hanya memberikan sisa parameternya, yaitu teks, sebagaimana baris terakhir keluaran tersebut. Perbaikannya adalah menghapus argumen yang nilainya telah dibekukan dari daftar pemanggilan.
Ringkasan#
- Metode
curry,rcurry, danncurrymengembalikan closure baru dengan sebagian argumen yang nilainya sudah dibekukan, sedangkan closure asal tetap utuh dan tetap dapat dipanggil. currymembekukan dari kiri,rcurrymembekukan dari kanan, danncurrymembekukan mulai indeks tertentu, termasuk beberapa nilai sekaligus.- Operator
<<menjalankan operand kanannya lebih dahulu,>>menjalankan operand kirinya lebih dahulu, danandThenmenjalankan closure penerimanya lebih dahulu. - Hasil komposisi adalah
ComposedClosure, yaitu closure biasa yang dapat dirantai, dicurry, atau dikirim sebagai argumen.
Referensi#
- Closures — Apache Groovy
— bagian currying dan komposisi yang menetapkan perilaku
curry,rcurry,ncurry, serta komposisi closure.