Currying dan aplikasi parsial

Mengapa setiap fungsi menerima satu argumen dan manfaat aplikasi parsial.

Daftar isi

Fungsi dengan dua masukan dalam Haskell sebenarnya adalah fungsi dengan satu masukan yang menghasilkan fungsi baru. Sifat ini disebut currying, diambil dari nama ahli logika Haskell Curry, dan akibat langsungnya adalah aplikasi parsial, yaitu pemanggilan fungsi dengan hanya sebagian argumennya. Topik ini menjelaskan pembacaan tipe fungsi bertingkat, manfaat aplikasi parsial, notasi section untuk operator, serta kesalahan yang lazim terjadi saat membaca pemanggilan fungsi.

Setiap fungsi menerima satu argumen#

Tipe Double -> Double -> Double tidak boleh dibaca sebagai “menerima dua argumen sekaligus”. Panah pada tipe mengerucut ke kanan, sehingga tipe tersebut sama saja dengan Double -> (Double -> Double). Pembacaannya berlapis, yaitu sebuah fungsi menerima Double, kemudian menghasilkan fungsi lain yang menerima Double dan menghasilkan Double. Dengan aturan ini, seluruh fungsi dalam Haskell menerima tepat satu argumen.

Pemanggilan bergerak ke arah sebaliknya. Ekspresi pangkat 2 8 dikelompokkan dari kiri menjadi (pangkat 2) 8, sehingga 2 disediakan lebih dahulu dan hasil sementaranya adalah fungsi, lalu fungsi tersebut menerima 8. Tidak ada tanda koma dan tidak ada tanda kurung yang menyatukan dua nilai menjadi satu paket. Penulisan f x y hanyalah singkatan dari pemanggilan berlapis tersebut.

haskell Nilai sela antara dua pemanggilan
-- tidak-dijalankan
-- pangkat 2 sudah bertipe fungsi, sehingga dapat diberi nama tersendiri
pangkatKuadrat :: Double -> Double
pangkatKuadrat = pangkat 2

Nilai pangkat 2 bukan perhitungan yang setengah jadi, melainkan nilai biasa yang bertipe fungsi. Fungsi dalam Haskell adalah nilai kelas satu (first-class value), artinya fungsi dapat diberi nama, disimpan, dan diteruskan ke fungsi lain. Nama pangkatKuadrat pada potongan di atas hanya memberi label pada nilai yang sudah tersedia, tanpa menulis ulang parameter apa pun.

Aplikasi parsial#

Aplikasi parsial (partial application) terjadi ketika fungsi hanya menerima sebagian argumennya. Hasilnya selalu fungsi yang masih menunggu sisa argumen. Pola ini digunakan untuk membuat fungsi khusus dari fungsi umum, sebagaimana program berikut yang menurunkan kuadrat dan akar dari satu fungsi pangkat.

haskell pangkat.hs
-- Fungsi dua argumen: menerima satu argumen dan menghasilkan fungsi baru
pangkat :: Double -> Double -> Double
pangkat p x = x ** p

-- Aplikasi parsial: argumen pertama disediakan lebih dahulu
kuadrat :: Double -> Double
kuadrat = pangkat 2

akar :: Double -> Double
akar = pangkat 0.5

main :: IO ()
main = do
  print (pangkat 2 8)
  print (kuadrat 8)
  print (akar 16)
  print (map kuadrat [1, 2, 3, 4])
text Keluaran
64.0
64.0
4.0
[1.0,4.0,9.0,16.0]

Definisi kuadrat = pangkat 2 tidak menyebut parameter apa pun, karena nilai yang diberi nama memang sudah bertipe Double -> Double. Pada baris terakhir, map kuadrat [1, 2, 3, 4] meneruskan fungsi hasil aplikasi parsial tersebut ke map, sehingga setiap elemen list dikuadratkan. Fungsi khusus dapat dibuat tanpa menulis definisi berulang, dan sisa argumen tetap disediakan pada tempat penggunaannya.

Bentuk yang paling sering muncul pada kode nyata adalah fungsi khusus yang disusun langsung di tempat pemanggilannya, misalnya map (pangkat 3) [1, 2, 3] yang membentuk fungsi kubik tanpa diberi nama karena hasil aplikasi parsial langsung diteruskan. Manfaat ini bergantung pada urutan parameter, sehingga kebiasaan yang lazim adalah meletakkan argumen yang jarang berubah di awal daftar argumen, sedangkan argumen yang berbeda pada setiap pemanggilan diletakkan di akhir. Fungsi dengan urutan demikian mudah diterapkan secara parsial, sementara fungsi dengan urutan terbalik harus dibungkus lambda lebih dahulu.

Section untuk operator#

Operator juga berupa fungsi, sehingga operator dapat diterapkan secara parsial dengan cara yang sama. Notasi ringkasnya disebut section, yaitu ekspresi berkurung yang menyisakan satu sisi operator kosong. Bentuk (/ 2) berarti “bagi dengan dua”, sedangkan bentuk (* 3) berarti “kalikan tiga”. Kedua sisi operator tidak setara, sehingga posisi yang disisakan menentukan arti section tersebut.

bash
# section: setiap nilai dibagi dua
ghc -e 'map (/ 2) [10, 15, 25]'
# section: setiap nilai dikali tiga
ghc -e 'map (* 3) [1, 2, 3]'
# section sebagai predikat penyaringan
ghc -e 'filter (> 5) [3, 7, 2, 9]'
text Keluaran
[5.0,7.5,12.5]
[3,6,9]
[7,9]

Section (/ 2) membagi masukan dengan dua, sedangkan section (2 /) membagi 2 dengan masukan. Perbedaannya terlihat jelas pada operator yang tidak komutatif seperti pembagian dan pengurangan. Predikat (> 5) pada filter berarti “lebih besar dari lima”, sehingga hanya 7 dan 9 yang lolos penyaringan.

Kesalahan umum#

Kesalahan pertama adalah menganggap f (x, y) sebagai cara memanggil fungsi dengan dua argumen. Tanda kurung pada (x, y) membentuk satu nilai tuple, sehingga pemanggilan itu meneruskan satu argumen berupa tuple. Program berikut menunjukkan fungsi yang memang dirancang untuk menerima satu pasangan.

haskell pasangan.hs
-- Fungsi dengan pola pasangan: menerima SATU argumen berupa tuple
rataRataPasangan :: (Double, Double) -> Double
rataRataPasangan (a, b) = (a + b) / 2

main :: IO ()
main = do
  print (rataRataPasangan (8, 6))
text Keluaran
7.0

Fungsi rataRataPasangan bertipe (Double, Double) -> Double, sehingga dipanggil dengan rataRataPasangan (8, 6). Bentuk rataRataPasangan 8 6 ditolak compiler karena tidak ada yang menampung argumen kedua. Argumen bergaya currying dan nilai yang digabung dalam tuple adalah dua hal yang berbeda, dan pilihannya terlihat langsung pada tipe fungsinya.

Kesalahan kedua adalah melupakan tanda kurung pada argumen yang berupa hasil fungsi. Ekspresi f g x berarti (f g) x, yaitu g diterapkan lebih dahulu sebagai argumen bagi f. Apabila hasil g x ingin diteruskan ke f, penulisannya adalah f (g x). Aplikasi fungsi memiliki prioritas tertinggi di antara semua operator, sehingga tanda kurung perlu ditulis secara eksplisit jika urutan penerapannya berbeda dari kiri ke kanan.

Ringkasan#

  • Semua fungsi dalam Haskell menerima tepat satu argumen, dan tipe banyak argumen dibaca sebagai fungsi yang menghasilkan fungsi.
  • Pemanggilan f x y dikelompokkan dari kiri menjadi (f x) y, sedangkan tipe a -> b -> c dikelompokkan dari kanan.
  • Aplikasi parsial menyediakan sebagian argumen dan menghasilkan fungsi siap diteruskan, misalnya kuadrat = pangkat 2.
  • Section seperti (/ 2) dan (> 5) adalah bentuk singkat aplikasi parsial untuk operator.
  • f (x, y) meneruskan satu tuple, bukan dua argumen, sehingga perbedaannya terlihat pada tipe fungsi.

Referensi#