Operator bind (>>=) adalah inti kelas Monad, yaitu kemampuan menyambungkan dua langkah komputasi dengan ketentuan bahwa langkah kedua dipanggil menggunakan isi konteks langkah pertama. Topik ini membahas tipe operator bind, penggunaannya pada Maybe untuk menyusun langkah yang setiap langkahnya dapat gagal, hubungan notasi do dengan bentuk bind, dan tiga hukum monad yang menjaga konsistensinya. Setelah membacanya, rantai langkah berkonteks dapat ditulis dalam bentuk bind maupun notasi do, dan kesetaraan kedua bentuk tersebut dapat dijelaskan.
Operator bind (>>=)#
Operator (>>=) bertipe m a -> (a -> m b) -> m b. Argumen pertamanya adalah konteks, sedangkan argumen keduanya adalah fungsi yang menerima isi konteks dan menghasilkan konteks baru. Perbedaan dengan fmap dan <*> terletak pada fungsi itu sendiri, karena fungsi pada bind tidak menghasilkan nilai murni, melainkan langsung menghasilkan konteks. Kemampuan itu dibutuhkan setiap kali langkah berikutnya tidak pasti bentuknya, misalnya fungsi yang menghasilkan Nothing untuk masukan tertentu dan Just untuk yang lain.
# bind meneruskan isi Just kepada fungsi yang menghasilkan Just
ghc -e 'Just 4 >>= \n -> Just (n * 2)'
# Nothing memutus rantai karena tidak ada isi yang diteruskan
ghc -e 'Nothing >>= \n -> Just (n * 2)'Just 8
NothingPada baris pertama, isi Just 4 diteruskan kepada fungsi lambda sehingga hasilnya Just 8. Pada baris kedua, Nothing tidak memiliki isi, sehingga fungsi tidak pernah dipanggil dan rantai berhenti dengan Nothing. Pola dua jalur ini berlaku untuk setiap instans Monad dengan ciri masing-masing, yaitu berhenti saat kosong pada Maybe, berhenti sambil membawa pesan pada Either, bercabang ke semua hasil pada list, dan berurutan pada IO. Selebihnya, operator ini bekerja sebagai satu konvensi penyambungan langkah yang sama di mana pun.
Perbedaannya dengan fmap terbaca dari tipe fungsi yang diterima. fmap menerima fungsi dari a ke b, sedangkan (>>=) menerima fungsi dari a ke m b yang langsung menghasilkan konteks. Fungsi semacam itu lazim disebut fungsi efek (effectful function) karena hasilnya selalu disertai konteks, misalnya hasil pembagian yang bisa gagal atau aksi yang mencetak ke layar. Apabila fungsi yang tersedia masih menghasilkan nilai murni, pembungkusan dengan return atau pure mengubahnya menjadi bentuk yang dapat disambungkan.
Rantai langkah pada Maybe#
Rantai panjang terbentuk tanpa tambahan kerumitan karena setiap langkah menghasilkan konteks yang siap disambungkan. Program berikut membaca teks menjadi angka, kemudian membagi dua angka yang genap saja. Kedua fungsi menghasilkan Maybe, sehingga keduanya disambungkan dengan bind dan kegagalan pada langkah mana pun memotong seluruh rantainya.
-- Bind menyambung langkah yang setiap langkahnya dapat gagal
bacaAngka :: String -> Maybe Int
bacaAngka teks = case reads teks of
[(n, "")] -> Just n
_ -> Nothing
setengah :: Int -> Maybe Int
setengah n
| even n = Just (n `div` 2)
| otherwise = Nothing
main :: IO ()
main = do
print (bacaAngka "24" >>= setengah)
print (bacaAngka "25" >>= setengah)
print (bacaAngka "seratus" >>= setengah)
-- rantai tiga langkah beruntun
print (bacaAngka "40" >>= setengah >>= setengah >>= setengah)Just 12
Nothing
Nothing
Just 5Baris pertama membaca "24" menjadi 24 lalu membaginya menjadi 12. Baris kedua gagal pada setengah karena 25 ganjil, sedangkan baris ketiga gagal lebih awal pada bacaAngka karena teksnya bukan bilangan. Baris keempat menyambungkan tiga langkah setengah beruntun atas 40, yang berhenti di 5 karena langkah keempat tidak ada. Setiap langkah bekerja pada nilai yang sudah melewati pemeriksaan langkah sebelumnya.
Tanpa bind, pola yang sama ditulis sebagai pencocokan pola bersarang yang bertambah satu tingkat setiap langkah. Rantai empat langkah menuntut empat tingkat case yang masing-masing menangani Nothing dengan cara serupa, dan penulisan itu cepat memenuhi layar tanpa menambah informasi. Bind menyimpan pola tersebut ke dalam satu operator, sehingga tambahan langkah hanya berupa satu sambungan baru, bukan satu lapisan bersarang. Kesamaan bentuk inilah yang membuat rantai Maybe tetap mudah dibaca meskipun panjangnya bertambah.
Notasi do dan bentuk bind#
Notasi do yang lazim muncul pada aksi IO ternyata tidak khusus untuk IO, melainkan gula sintaks (syntactic sugar) yang disusun di atas bind. Program berikut menuliskan rantai yang sama dalam dua bentuk, yaitu notasi do dan rantai bind dengan lambda, lalu membandingkan hasil keduanya.
-- Notasi do dan bentuk bind yang setara
bacaAngka :: String -> Maybe Int
bacaAngka teks = case reads teks of
[(n, "")] -> Just n
_ -> Nothing
setengah :: Int -> Maybe Int
setengah n
| even n = Just (n `div` 2)
| otherwise = Nothing
total :: Maybe Int
total = do
a <- bacaAngka "24"
b <- setengah a
c <- setengah b
return (a + b + c)
totalBind :: Maybe Int
totalBind =
bacaAngka "24" >>= \a ->
setengah a >>= \b ->
setengah b >>= \c ->
return (a + b + c)
main :: IO ()
main = do
print total
print totalBind
print (total == totalBind)Just 42
Just 42
TrueBaris a <- bacaAngka "24" pada notasi do menyatakan pengambilan isi konteks dan pemberian nama a untuk isinya, sedangkan baris return (a + b + c) membungkus hasilnya kembali menjadi konteks. Setiap baris berpola a <- ... dikompilasi menjadi ... >>= \a -> ..., sehingga kedua definisi total dan totalBind menyusun perhitungan yang persis sama. Perbandingan terakhir bernilai True karena keduanya menghasilkan Just 42 dari angka 24, 12, dan 6. Kesetaraan ini berarti notasi do hanya mempermudah pembacaan dan tidak menambahkan kemampuan baru.
Hukum monad#
Sebagaimana Functor, instans Monad memiliki hukum yang menjaga perilakunya tetap terduga. Fungsi return pada kelas Monad memiliki peran yang sama dengan pure pada Applicative, yaitu mengangkat nilai murni ke dalam konteks. Hukum identitas kiri menyatakan return x >>= f sama dengan f x, artinya membungkus lalu meneruskan sama dengan menerapkan langsung. Hukum identitas kanan menyatakan m >>= return sama dengan m, artinya menyambungkan pembungkus di akhir rantai tidak mengubah apa pun. Hukum asosiatif menyatakan tanda kurung pada dua bind beruntun boleh dipindahkan, sehingga rantai panjang dapat dikelompokkan tanpa mengubah hasilnya.
# hukum identitas kiri: membungkus lalu meneruskan sama dengan menerapkan langsung
ghc -e '(return 5 >>= \x -> Just (x * 2)) == Just 10'
# hukum identitas kanan: menyambung return di akhir tidak mengubah apa pun
ghc -e '(Just 5 >>= return) == Just 5'
# hukum asosiatif: tanda kurung dua bind beruntun boleh dipindahkan
ghc -e '((Just 2 >>= \x -> Just (x + 3)) >>= \y -> Just (y * 2)) == (Just 2 >>= (\x -> Just (x + 3) >>= \y -> Just (y * 2)))'True
True
TrueKetiga perbandingan bernilai True pada instans Maybe. Hukum-hukum ini bukan pemeriksaan compiler, melainkan janji yang wajib dipatuhi penulis instans. Karena itu, pemfaktoran ulang seperti memindahkan tanda kurung pada rantai bind atau mengganti notasi do dengan bentuk bind tidak mengubah perilaku program yang instansnya benar. Instans yang melanggar hukum memang dapat dibuat, tetapi kode yang mengandalkannya menjadi sulit diprediksi karena penyusunan ulang rantainya bisa mengubah hasil.
Ringkasan#
(>>=)bertipem a -> (a -> m b) -> m bdan meneruskan isi konteks kepada fungsi yang menghasilkan konteks baru.- Pada
Maybe, satu kegagalan memotong seluruh rantai bind tanpa langkah berikutnya dipanggil. - Notasi
doadalah gula sintaks di atas bind, sehingga barisa <- msetara denganm >>= \a -> .... - Instans
Monadwajib mematuhi identitas kiri, identitas kanan, dan asosiatif, dan ketiganya dapat diuji dengan perbandingan biasa.
Referensi#
- Control.Monad — Hackage
— Dokumentasi resmi kelas
Monadbeserta(>>=)dan fungsi-fungsi pendukungnya. - Monad — HaskellWiki
— Halaman wiki komunitas yang menjelaskan kelas
Monadbeserta hukum dan contohnya. - base package — Hackage
— Halaman utama library standar
basetempat kelasMonaddideklarasikan.