Gaya applicative adalah cara menyusun komputasi yang setiap argumennya berada di dalam konteks, dibangun di atas Functor melalui kelas Applicative. Topik ini membahas pure yang mengangkat nilai murni ke dalam konteks, operator <*> yang menerapkan fungsi terbungkus, serta pola f <$> x <*> y untuk fungsi beberapa argumen pada Maybe, list, dan IO. Setelah membacanya, fungsi dua argumen atau lebih dapat diterapkan pada nilai yang membawa konteks tanpa membuka pembungkusnya secara manual satu per satu.
pure: mengangkat nilai ke dalam konteks#
Fungsi pure bertipe a -> f a dan menjadi pintu masuk paling sederhana ke dalam konteks. Fungsi ini menerima nilai murni apa pun dan menempatkannya ke dalam konteks type constructor yang diminta. Nilai 4 yang dianotasi :: Maybe Int menjadi Just 4, nilai yang sama dengan anotasi :: [Int] menjadi list berisi satu elemen, sedangkan pada IO nilai itu menjadi aksi yang tidak melakukan efek apa pun selain menghasilkan 4. Konteks terkecil yang dibentuk pure inilah yang membuat perilakunya berbeda antartipe.
# pure menempatkan nilai murni di dalam konteks yang diminta
ghc -e 'pure 4 :: Maybe Int'
ghc -e 'pure 4 :: [Int]'
ghc -e 'pure 4 :: IO Int'Just 4
[4]
4Tiga baris keluaran menunjukkan bahwa pure selalu menghasilkan konteks paling sederhana dari tipe yang diminta. Maybe memperoleh Just, list memperoleh satu elemen, dan IO memperoleh aksi yang langsung menghasilkan nilainya apabila dievaluasi oleh ghc -e. Fungsi pure dideklarasikan pada Control.Applicative dan juga diekspor melalui Prelude, sehingga pemanggilannya tidak memerlukan pernyataan impor tambahan. Setiap type constructor yang menjadi instans Applicative wajib menentukan bagaimana nilai murni itu ditempatkan di dalamnya.
Operator <*>: fungsi di dalam konteks#
Kelas Applicative menambahkan operator <*> bertipe f (a -> b) -> f a -> f b. Operator ini menerapkan fungsi yang ikut terbungkus konteks terhadap nilai yang juga terbungkus konteks. Kemampuan barunya tampak pada fungsi beberapa argumen, karena setiap argumen berada di dalam konteksnya sendiri dan operator ini menyambungkannya satu per satu. Pola yang lazim menuliskan fungsi murni di awal melalui <$>, lalu menyambung setiap argumen berikutnya dengan <*>.
-- Applicative pada list: setiap kombinasi fungsi dan nilai dikunjungi
main :: IO ()
main = do
-- satu fungsi atas dua list: setiap pasangan dicoba
print ((*) <$> [2, 3] <*> [10, 20, 30])
-- beberapa fungsi atas satu list
print ([(* 2), (+ 10)] <*> [1, 2])[20,40,60,30,60,90]
[2,4,11,12]List menunjukkan sifat applicative yang paling terlihat, yaitu setiap kombinasi fungsi dan nilai dikunjungi. Ekspresi pertama menerapkan perkalian pada setiap pasangan dari [2, 3] dan [10, 20, 30], sehingga enam hasil muncul berurutan sesuai urutan elemen sumbernya. Ekspresi kedua menaruh dua fungsi di dalam list, dan kedua fungsi itu diterapkan pada kedua elemen [1, 2] sehingga empat hasil muncul. Pola ini terbaca sebagai penelusuran sistematis atas semua kemungkinan, dan urutannya tetap teratur karena mengikuti susunan list sumbernya.
Rantai <*> yang panjang dimungkinkan oleh aplikasi parsial, yaitu penerapan fungsi yang baru mengisi sebagian argumennya. Ekspresi (+) <$> Just 4 menghasilkan Just (4 +) yang bertipe Maybe (Int -> Int), yaitu konteks yang berisi fungsi satu argumen hasil menambatkan argumen pertamanya. Operator <*> selanjutnya menyambungkan konteks fungsi itu dengan konteks argumen berikutnya, dan pola yang sama berlanjut untuk argumen ketiga dan seterusnya. Setiap sambungan mengurangi jumlah argumen yang belum terisi, sampai fungsi tersebut utuh dan konteksnya berisi nilai akhir.
Menggabungkan dua konteks pada Maybe#
Pada Maybe, pola f <$> x <*> y menggabungkan dua hasil pencarian dalam satu ekspresi. Program berikut menghitung selisih dua nilai siswa dari sebuah kamus, lengkap dengan satu kasus yang nilainya tidak ditemukan.
-- Gaya applicative: fungsi beberapa argumen atas nilai yang membungkusnya
nilaiSiswa :: [(String, Int)]
nilaiSiswa = [("Ali", 85), ("Budi", 78)]
nilai :: String -> Maybe Int
nilai nama = lookup nama nilaiSiswa
main :: IO ()
main = do
-- satu fungsi dua argumen atas dua nilai Maybe
print ((-) <$> nilai "Ali" <*> nilai "Budi")
print ((+) <$> nilai "Ali" <*> nilai "Citra")
-- pure mengangkat nilai murni ke dalam konteks
print ((+) <$> nilai "Ali" <*> pure 15)Just 7
Nothing
Just 100Ekspresi pertama menghitung selisih nilai Ali dan Budi karena keduanya ditemukan di kamus, sedangkan ekspresi kedua menghasilkan Nothing karena nilai Citra tidak ada. Nilai Nothing pada salah satu argumen memotong seluruh ekspresi tanpa satu pun fungsi dijalankan, sehingga tidak ada cabang pemeriksaan manual yang ditulis. Ekspresi ketiga menunjukkan peran pure, yaitu mengangkat angka murni 15 menjadi argumen konteks Maybe agar dapat disambungkan dengan <*>. Ketiga ekspresi menggunakan fungsi aritmetika biasa tanpa satu pun pencocokan pola.
Fungsi liftA2 menyediakan bentuk yang lebih pendek untuk fungsi dua argumen.
# liftA2 menerapkan fungsi dua argumen dalam satu panggilan
ghc -e 'liftA2 (+) (Just 4) (Just 3)'
ghc -e 'liftA2 min (Just 5) (Just 2)'Just 7
Just 2Applicative pada IO dan batas kemampuannya#
Pola yang sama berlaku pada IO, yaitu beberapa aksi digabungkan menjadi satu ekspresi yang hasilnya dihitung bersama. Program berikut menggabungkan dua aksi sederhana dengan fungsi penjumlahan murni, sehingga keluarannya tetap dapat direproduksi.
-- Applicative pada IO: beberapa aksi digabung tanpa saling bergantung
main :: IO ()
main = do
hasil <- pure (+) <*> pure 20 <*> pure 22
print hasil42Aksi pure 20 dan pure 22 masing-masing menghasilkan nilai sederhana, sedangkan pure (+) menyediakan fungsi penjumlahannya, dan print mencetak hasil gabungannya. Susunan ini bekerja dengan baik selama setiap aksi dapat ditulis tanpa mengetahui hasil aksi lain. Batas kemampuan applicative justru terletak di situ, karena bentuk komputasinya sudah tetap sejak awal sehingga aksi kedua tidak dapat berubah isi berdasarkan hasil aksi pertama. Apabila langkah kedua bergantung pada hasil langkah pertama, kemampuan tersebut diberikan oleh operator (>>=) dari kelas Monad.
Batas tersebut dapat dinyatakan tanpa menghafal aturan tambahan. Pada f <$> x <*> y, konteks y harus lengkap sebelum ekspresi disusun, sedangkan kebutuhan nyata sering berupa langkah kedua yang menghitung sesuai hasil langkah pertama. Tipe <*> tidak menyediakan tempat untuk ketergantungan semacam itu karena konteks keduanya sudah berdiri sendiri. Keterbatasan ini bukan kekurangan desain, melainkan batas yang membuat bentuk komputasi applicative dapat dianalisis sepenuhnya dari tipenya tanpa menjalankannya.
Ringkasan#
puremengangkat nilai murni ke dalam konteks terkecil dari type constructor yang diminta, misalnyaJust 4untukMaybe Intdan[4]untuk[Int].- Operator
<*>menerapkan fungsi terbungkus terhadap nilai terbungkus, dan polaf <$> x <*> ymenyambungkan fungsi murni dengan setiap argumennya. - Instans list menelusuri semua kombinasi fungsi dan nilai, sedangkan instans
Maybememotong ekspresi apabila salah satu argumennyaNothing. - Gaya applicative menyusun bentuk komputasi yang tetap sejak awal, sehingga langkah yang isinya bergantung pada hasil sebelumnya membutuhkan
Monad.
Referensi#
- Control.Applicative — Hackage
— Dokumentasi resmi kelas
Applicativeyang memuatpure,<*>, danliftA2. - Data.Functor — Hackage
— Dokumentasi resmi operator
<$>yang memetakan fungsi di dalam konteks. - base package — Hackage
— Halaman utama library standar
basetempat kelasApplicativedideklarasikan.