Komposisi fungsi (function composition) menyusun dua fungsi menjadi satu fungsi baru, yaitu keluaran fungsi di sisi kanan diteruskan kepada fungsi di sisi kiri. Operator (.) menyatakan komposisi tersebut dan menjadi dasar gaya penulisan tanpa parameter yang disebut point-free. Topik ini membahas mekanisme operator (.), penggunaannya untuk merapikan rantai transformasi list, serta pertimbangan menilai kapan gaya point-free memperjelas kode dan kapan justru menyembunyikannya.
Operator komposisi (.)#
Definisi operator (.) singkat, yaitu (f . g) x = f (g x), dan tipenya adalah (b -> c) -> (a -> b) -> a -> c. Komposisi f . g menghasilkan fungsi baru yang menerima masukan x, menghitung g x terlebih dahulu, lalu menghitung f atas hasilnya. Arah pembacaannya dari kanan ke kiri, sehingga gandakanDua . tambahTiga berarti tambah tiga terlebih dahulu, lalu gandakan dua.
-- (.) menyusun dua fungsi, fungsi di kanan dihitung lebih dahulu
gandakanDua :: Int -> Int
gandakanDua n = n * 2
tambahTiga :: Int -> Int
tambahTiga n = n + 3
main :: IO ()
main = do
print ((gandakanDua . tambahTiga) 5)
print (gandakanDua (tambahTiga 5))
print ((tambahTiga . gandakanDua) 5)16
16
13Dua baris pertama keluaran menegaskan bahwa (gandakanDua . tambahTiga) 5 menghitung hal yang sama dengan gandakanDua (tambahTiga 5). Baris ketiga membalik urutan komposisinya, dan hasilnya berubah dari 16 menjadi 13 karena penerapannya tidak simetris. Karena . bersifat asosiatif kanan, rantai f . g . h dibaca (f . (g . h)) tanpa memerlukan tanda kurung tambahan, sehingga komposisi tiga fungsi atau lebih tetap ditulis sebagai satu rantai tanpa penanda urutan.
Fungsi identitas id menyatu secara alami dengan komposisi, karena f . id dan id . f sama saja dengan f, sehingga id menjadi nilai netral bagi operator (.). Sifat ini berguna ketika tahapan disusun secara dinamis, misalnya list tahapan yang boleh kosong digantikan dengan id, sehingga komposisinya tetap sah tanpa kasus khusus.
Merantai transformasi#
Rantai komposisi paling berguna ketika data mengalir melalui beberapa tahap transformasi. Contoh yang lazim adalah menyaring, memetakan, lalu menjumlahkan. Fungsi jumlahKuadratGenap menerima list, menyaring bilangan genap, mengkuadratkan setiap hasilnya, dan menjumlahkannya. Gaya komposisi dan gaya bersarang ditulis berdampingan untuk menunjukkan bahwa keduanya menghitung hal yang sama.
-- Rantai komposisi: saring, petakan, lalu jumlahkan
kuadrat :: Int -> Int
kuadrat n = n * n
jumlahKuadratGenap :: [Int] -> Int
jumlahKuadratGenap = sum . map kuadrat . filter even
versiBersarang :: [Int] -> Int
versiBersarang xs = sum (map kuadrat (filter even xs))
main :: IO ()
main = do
print (jumlahKuadratGenap [1 .. 6])
print (versiBersarang [1 .. 6])56
56Definisi pertama membaca arus data dari kanan ke kiri, yaitu filter even bekerja dahulu, dilanjutkan map kuadrat, dan berakhir di sum. Definisi kedua membaca dari dalam ke luar dengan tanda kurung tiga lapis. Keduanya sah, tetapi rantai komposisi menyeragamkan bentuk setiap tahapnya, sehingga penambahan atau pembuangan tahap cukup mengubah satu bagian rantai. Anotasi tipe jumlahKuadratGenap :: [Int] -> Int menyatakan keseluruhan rantai sebagai satu fungsi dari list ke angka.
Komposisi juga berlaku untuk fungsi aritmetika sebagaimana fungsi list. Perintah berikut menyusun tiga tahap dan menghitungnya dari kanan ke kiri, yaitu masukan tiga ditambah satu menjadi empat, dikalikan dua menjadi delapan, lalu ditambah satu menjadi sembilan.
# komposisi tiga fungsi aritmetika, dihitung dari kanan ke kiri
ghc -e '((+ 1) . (* 2) . (+ 1)) 3'9Gaya point-free#
Fungsi dinyatakan dalam gaya point-free apabila parameternya tidak pernah disebut, dan tubuhnya hanya berupa komposisi atau aplikasi parsial dari fungsi lain. Istilah point merujuk pada variabel argumen, sehingga tanpa point berarti tanpa argumen yang eksplisit. Program berikut menuliskan perhitungan yang sama dalam dua gaya untuk menunjukkan hubungan keduanya.
-- Point-free: fungsi didefinisikan tanpa menyebut parameternya
kuadrat :: Int -> Int
kuadrat n = n * n
totalBersarang :: [Int] -> Int
totalBersarang xs = sum (map kuadrat (filter even xs))
totalPointFree :: [Int] -> Int
totalPointFree = sum . map kuadrat . filter even
main :: IO ()
main = do
print (totalBersarang [1 .. 5])
print (totalPointFree [1, 1, 1, 1, 1])
print (map (negate . abs) [-2, 3, -4])
print . sum $ [1, 2, 3]20
0
[-2,-3,-4]
6Definisi totalPointFree tidak menyebut satu parameter pun karena sum . map kuadrat . filter even sudah merupakan fungsi lengkap yang menunggu list. Bentuk serupa muncul pada baris terakhir, yaitu print . sum $ [1, 2, 3] yang menyusun komposisi print . sum lalu menyediakan argumennya melalui operator $. Aplikasi parsial juga terlihat pada map (negate . abs), yang menerapkan komposisi dua fungsi unari kepada setiap elemen dan menghasilkan nilai mutlak yang diberi tanda negatif.
Kapan point-free memperjelas#
Point-free memperjelas kode apabila setiap bagian komposisinya berupa fungsi yang namanya sudah menjelaskan perannya. Definisi totalPointFree = sum . map kuadrat . filter even terbaca sebagai resep tiga tahap tanpa nama variabel yang mengganggu. Sebaliknya, komposisi panjang dengan section bertumpuk justru menuntut pembaca menyusun ulang tipenya secara mental. Tabel berikut merangkum pertimbangannya.
| Situasi | Pilihan yang lazim |
|---|---|
| Dua hingga tiga tahap dengan fungsi bernama yang jelas | point-free, misalnya sum . map kuadrat |
| Rantai panjang dengan section yang bertumpuk | parameter eksplisit dengan nama tahap yang jelas |
| Fungsi yang perlu dianotasi tipenya dan diuji | definisi bernama dengan anotasi tipe lengkap |
Tiga pertanyaan praktis membantu menilai sebuah definisi point-free. Pertama, apakah setiap tahapnya masih dapat disebut dengan nama yang bermakna. Kedua, apakah tipe hasil akhirnya masih dapat ditebak tanpa menghitung tipe secara manual. Ketiga, apakah pesan error compiler tetap mudah dibaca apabila salah satu tahap salah tulis. Apabila ada pertanyaan yang jawabannya tidak, kembalikan parameternya dan beri nama pada tahap yang rumit. Anotasi tipe tetap membantu pada kedua gaya karena menyatakan maksud definisi tanpa perlu membaca rantainya.
Ringkasan#
(f . g) xsama denganf (g x), dihitung dari kanan ke kiri, dan bersifat asosiatif kanan sehingga rantai panjang tidak memerlukan tanda kurung.- Rantai komposisi merapikan transformasi bertahap seperti saring, petakan, dan jumlahkan menjadi satu definisi dengan anotasi tipe yang ringkas.
- Gaya point-free menulis fungsi tanpa parameter eksplisit dan paling terbaca apabila setiap tahapnya berupa fungsi bernama.
- Komposisi panjang atau section bertumpuk lebih baik ditulis dengan parameter eksplisit dan nama tahap yang jelas.
Referensi#
- Prelude — Hackage
— Dokumentasi operator
(.)beserta tipenya pada library standarbase. - Pointfree — Haskell Wiki — Penjelasan gaya point-free beserta contoh dan batasannya.
- Haskell 2010 Language Report, Bab 3 — haskell.org — Aturan ekspresi dan asosiativitas operator pada spesifikasi resmi bahasa.