Gaya Applicative

pure, <$>, dan <*> untuk nilai yang membawa konteks.

Daftar isi

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.

bash
# 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'
text Keluaran
Just 4
[4]
4

Tiga 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 <*>.

haskell kombinasi-list.hs
-- 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])
text Keluaran
[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.

haskell applicative-maybe.hs
-- 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)
text Keluaran
Just 7
Nothing
Just 100

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

bash
# liftA2 menerapkan fungsi dua argumen dalam satu panggilan
ghc -e 'liftA2 (+) (Just 4) (Just 3)'
ghc -e 'liftA2 min (Just 5) (Just 2)'
text Keluaran
Just 7
Just 2

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

haskell applicative-io.hs
-- Applicative pada IO: beberapa aksi digabung tanpa saling bergantung
main :: IO ()
main = do
  hasil <- pure (+) <*> pure 20 <*> pure 22
  print hasil
text Keluaran
42

Aksi 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#

  • pure mengangkat nilai murni ke dalam konteks terkecil dari type constructor yang diminta, misalnya Just 4 untuk Maybe Int dan [4] untuk [Int].
  • Operator <*> menerapkan fungsi terbungkus terhadap nilai terbungkus, dan pola f <$> x <*> y menyambungkan fungsi murni dengan setiap argumennya.
  • Instans list menelusuri semua kombinasi fungsi dan nilai, sedangkan instans Maybe memotong ekspresi apabila salah satu argumennya Nothing.
  • Gaya applicative menyusun bentuk komputasi yang tetap sejak awal, sehingga langkah yang isinya bergantung pada hasil sebelumnya membutuhkan Monad.

Referensi#