Hierarki

Functor, Applicative, dan Monad sebagai kekuatan yang bertambah bertahap.

Daftar isi

Hierarki Functor, Applicative, dan Monad adalah rangkaian type class yang mengatur cara fungsi mengolah nilai yang berada di dalam konteks. Topik ini memperkenalkan ketiga kelas tersebut sebagai tiga kekuatan yang bertambah bertahap, mulai dari menerapkan satu fungsi, menggabungkan beberapa konteks, hingga memilih langkah berikutnya berdasarkan hasil sebelumnya. Setelah membacanya, tipe operasi fmap, pure dengan <*>, dan (>>=) dapat dibaca dengan maknanya, dan program penutup memperlihatkan ketiga kekuatan itu bekerja pada tipe Maybe yang sama.

Tipe yang membungkus nilai#

Pembahasan ketiga kelas ini dimulai dari tipe yang membungkus tipe lain. Maybe Int membungkus satu Int dengan kemungkinan kosong, [Int] membungkus nol atau lebih Int, sedangkan IO Int menyatakan aksi yang apabila dijalankan menghasilkan Int. Tipe semacam itu disusun oleh type constructor, yaitu konstruktor tipe yang menerima parameter tipe sebelum menjadi tipe utuh. Maybe sendiri belum merupakan tipe lengkap karena baru menyatakan keluarga tipe setelah diisi Int, String, atau tipe lainnya.

Sepanjang topik ini, kata konteks digunakan untuk menyebut pembungkus yang menyertai nilai, yaitu kekosongan pada Maybe, jumlah elemen yang tidak tetap pada list, dan urutan efek pada IO. Nilai yang sama dapat berada di dalam konteks yang berbeda, dan konteks itulah yang menentukan perilaku pemrosesannya. Perintah berikut menampilkan satu nilai 4 di dalam tiga konteks berbeda.

bash
# nilai yang sama berada di dalam konteks yang berbeda
ghc -e 'Just 4 :: Maybe Int'
ghc -e '[4] :: [Int]'
ghc -e 'return 4 :: IO Int'
text Keluaran
Just 4
[4]
4

Tiga baris keluaran menampilkan nilai yang sama, tetapi maknanya berbeda karena konteksnya berbeda. Just 4 menyatakan nilai yang ada di dalam konteks yang mungkin kosong, [4] menyatakan satu elemen di dalam urutan yang bisa lebih panjang, sedangkan baris ketiga dihasilkan dari aksi IO yang dievaluasi oleh ghc -e. Pembedaan ini penting karena ketiga kelas pada topik ini bekerja pada bentuk f a yang sama, yaitu type constructor f yang membungkus tipe a.

Kekuatan yang bertambah bertahap#

Setiap kelas pada hierarki menambah satu kemampuan di atas kelas sebelumnya. Functor dapat menerapkan satu fungsi terhadap isi konteks. Applicative menambah kemampuan menerapkan fungsi yang menerima lebih dari satu argumen, dengan setiap argumen berada di dalam konteksnya masing-masing. Monad menambah kemampuan menentukan langkah berikutnya berdasarkan hasil langkah sebelumnya, kemampuan yang tidak dimiliki dua kelas di atasnya karena bentuk komputasinya sudah tetap sejak awal.

Bentuk paling ringkas dari ketiga kemampuan itu terlihat pada tipe operasi kuncinya. Anotasi berikut diambil dari deklarasi ketiga kelas pada library standar base.

haskell Tipe operasi kunci ketiga kelas
-- tidak-dijalankan
-- Functor: satu fungsi diterapkan pada satu konteks
fmap :: Functor f => (a -> b) -> f a -> f b
-- Applicative: nilai diangkat ke konteks, fungsi terbungkus diterapkan
pure :: Applicative f => a -> f a
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
-- Monad: langkah berikutnya dipanggil dengan isi konteks sebelumnya
(>>=) :: Monad m => m a -> (a -> m b) -> m b

fmap menerima fungsi dari a ke b beserta konteks f a, lalu mengembalikan konteks yang isinya sudah menjadi hasil penerapan fungsi tersebut. Operator <*> melangkah lebih jauh karena fungsinya sendiri ikut terbungkus, dan untuk fungsi dua argumen bentuknya menjadi f <$> x <*> y, yaitu fungsi murni dimasukkan melalui <$> lalu setiap argumen disambungkan dengan <*>. Operator (>>=) berbeda dari keduanya karena fungsi yang diterimanya menghasilkan konteks baru, sehingga bentuk langkah kedua boleh bergantung pada isi langkah pertama.

Perbedaan terakhir inilah pembeda utama ketiga kelas. Pada Functor dan Applicative, bentuk keseluruhan komputasi sudah tetap sejak ekspresi ditulis, yaitu fungsi yang sama diterapkan pada struktur yang sama tanpa melihat isinya. Pada Monad, fungsi menerima isi konteks terlebih dahulu, sehingga langkah kedua boleh berupa Just untuk satu masukan dan Nothing untuk masukan lain, atau aksi yang berbeda untuk hasil yang berbeda. Kekuatan yang bertambah berarti pula aturan yang semakin longgar, dan bagian berikutnya menunjukkan perbedaannya pada satu program yang sama.

Tiga kelas pada program yang sama#

Program berikut menerapkan ketiga kekuatan tersebut pada nilai Maybe yang sama. Dua kekuatan pertama menghasilkan keluaran yang identik karena menambahkan 3 pada Just 4 tidak menuntut keputusan apa pun, sedangkan kekuatan ketiga mulai tampak berbeda apabila langkah berikutnya bergantung pada hasil, misalnya fungsi langkah yang menolak nilai nol dan negatif.

haskell tiga-kekuatan.hs
-- Tiga kelas dengan kekuatan bertambah: Functor, Applicative, Monad
nilai :: Maybe Int
nilai = Just 4

langkah :: Int -> Maybe Int
langkah n
  | n > 0     = Just (n * 2)
  | otherwise = Nothing

main :: IO ()
main = do
  -- Functor: satu fungsi diterapkan pada isi konteks
  print (fmap (+ 3) nilai)
  -- Applicative: fungsi dua argumen diterapkan pada dua konteks
  print ((+) <$> nilai <*> Just 3)
  -- Monad: langkah berikutnya dipilih berdasarkan hasil langkah pertama
  print (nilai >>= langkah)
  print (Nothing >>= langkah)
text Keluaran
Just 7
Just 7
Just 8
Nothing

Baris pertama memetakan (+ 3) ke dalam Just 4 melalui fmap dan menghasilkan Just 7. Baris kedua menggabungkan fungsi penjumlahan dengan dua konteks melalui pola <$> dan <*>, dan hasilnya tetap Just 7 karena tidak ada keputusan yang bergantung pada isi. Baris ketiga menyambung Just 4 dengan langkah melalui (>>=), sehingga fungsi langkah dipanggil dengan isi konteks dan menghasilkan Just 8. Baris keempat menyambung Nothing dengan langkah yang sama, dan langkah tidak pernah dipanggil karena Nothing tidak memiliki isi untuk diteruskan. Kemampuan memutus perhitungan begitu konteks kosong adalah bentuk paling sederhana dari kekuatan Monad.

Empat baris keluaran itu juga memperlihatkan tipe yang tidak berubah di sepanjang program. Seluruh ekspresi menghasilkan Maybe Int, sedangkan perbedaannya hanya pada cara isi konteksnya diperoleh. Dua kekuatan pertama menyusun hasil dengan bentuk yang sudah tertulis di ekspresinya, sedangkan kekuatan ketiga menyisakan satu keputusan kepada fungsi langkah pada saat nilainya tersedia. Pemilihan kelas menjadi pertimbangan gaya, yaitu kemampuan secukupnya untuk kebutuhan yang ada.

Perbandingan ketiga kelas#

Tabel berikut merangkum ketiga kelas tersebut beserta pertanyaan yang dijawabnya.

KelasOperasi kunciPertanyaan yang dijawabContoh pada Maybe
Functorfmapbagaimana satu fungsi masuk ke dalam konteksfmap (+ 3) (Just 4)
Applicativepure dan <*>bagaimana beberapa konteks digabungkan(+) <$> Just 4 <*> Just 3
Monad(>>=)bagaimana langkah berikutnya ditentukanJust 4 >>= langkah

Urutan pada tabel sekaligus menyatakan urutan kekuatannya. Pada library standar saat ini, setiap instans Monad otomatis juga instans Applicative, dan setiap instans Applicative juga instans Functor, karena kelas yang lebih tinggi menjadi syarat bagi kelas di bawahnya. Ketentuan itu membuat ketiga operasi selalu tersedia pada tipe seperti Maybe, list, dan IO, sehingga yang dipertimbangkan hanyalah kekuatan yang secukupnya. Gunakan Functor apabila itu cukup membuat bentuk komputasi terlihat jelas, sedangkan Monad digunakan apabila langkah berikutnya memang bergantung pada hasil langkah sebelumnya.

Ringkasan#

  • Ketiga kelas mengolah nilai yang berada di dalam konteks, yaitu pembungkus seperti Maybe, list, dan IO yang dibentuk oleh type constructor.
  • Functor menyediakan fmap untuk menerapkan satu fungsi pada isi konteks.
  • Applicative menambahkan pure dan <*> agar fungsi beberapa argumen dapat diterapkan pada beberapa konteks sekaligus.
  • Monad menambahkan (>>=) agar langkah berikutnya dipanggil berdasarkan isi konteks sebelumnya, kemampuan yang tidak dimiliki dua kelas di atasnya.

Referensi#