Currying dan komposisi

Membangun fungsi parsial dengan curry, rcurry, ncurry, serta rantai andThen.

Daftar isi

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.

groovy curry-dasar.groovy
// 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}"
text Keluaran
Hasil baru : holahola
Setara     : holahola
Param baru : 1
Param asli : 2

Nilai 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.

groovy rcurry-ncurry.groovy
// 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)}"
text Keluaran
rcurry : 5
ncurry : 24.0
Setara : 24.0

Pada 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.

MetodePosisi nilai yang dibekukanContoh pada topik ini
currydari parameter paling kiriulang.curry(2)
rcurrydari parameter paling kananbagi.rcurry(2)
ncurrymulai indeks tertentu, dapat lebih dari satu nilaivolume.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.

groovy Contoh: ncurry dengan dua nilai yang dibekukan
// 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"
text Keluaran
100 C = 212.0 F
0 C   = 32.0 F
25 C  = 298.15 K

Komposisi 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.

groovy komposisi.groovy
// 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}"
text Keluaran
<< : 11
>> : 15
andThen: 15
Tipe   : ComposedClosure

Pada 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.

groovy Contoh: rantai andThen pada pipeline teks
// 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  ')
text Keluaran
[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.

groovy Contoh: pembacaan arah komposisi yang keliru
// 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))}"
text Keluaran
tambahDua << kaliTiga : 11
Pembacaan keliru      : 15
Pembacaan benar       : 11

Nilai 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.

groovy Contoh: argumen yang sudah dibekukan
// 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')}"
text Keluaran
Gagal: MissingMethodException
Benar: holahola

Pemanggilan 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, dan ncurry mengembalikan closure baru dengan sebagian argumen yang nilainya sudah dibekukan, sedangkan closure asal tetap utuh dan tetap dapat dipanggil.
  • curry membekukan dari kiri, rcurry membekukan dari kanan, dan ncurry membekukan mulai indeks tertentu, termasuk beberapa nilai sekaligus.
  • Operator << menjalankan operand kanannya lebih dahulu, >> menjalankan operand kirinya lebih dahulu, dan andThen menjalankan 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.