在看 monad 相關的文章時不時想到一個剛開始學 FP 時聽過的笑話 「a monad is just a monoid in the category of endofunctors」 ,隨著對於 FP 的認識逐漸加深會知道這句話的確就是一個正確的廢話。
如同 Functor
、Monoid
、Semigroup
、Applicative
這些抽象概念或者 typeclass 一樣, monad 本身也是一種在數學上的抽象概念。
那 monad 能做什麼事情?試想一下在我們之前使用 Applicative
我們都需要使用到 pure
讓我們把沒有 context 轉換成一樣的 context 才能方便我們接下來的計算
pure (*) <*> Just 2 <*> Just 2 -- Just 4
然而在某些情況下我們可能會遇到 function 會有失敗的可能性,所以必須使用到 Maybe
而Applicative
這時就顯得不太夠用。
例如這個 function
divide :: Int -> Int -> Maybe Int
divide _ 0 = Nothing
divide x y = Just (x `div` y)
我們定義了一個 divide
可以接受兩個 Int
但最後可能會是失敗的結果所以使用 Maybe
,像是除以 0
會回傳 Nothing
。
首先我們先來看一下這個運算子 (>>=)
(唸作 Bind)
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
簡單解釋就是 (>>=)
接收 m a
及 a → m b
最後會回傳 m b
,也就是接收一個被 Monad
所 wrap 住的 a
以及一個接收 a
並回傳被 Monad
wrap 的 b 的函數,最後則回傳被 Monad
wrap 的 b 。
是不是看起來有點跟 fmap
以及 <*>
有一點點像,前者在意的是我要把一個 function apply 到一個context 裡最後回傳同一種 context 的值,後者在意的是我傳入一個在 context 裡的 function 並把它給 apply 到同一種 context 的值最後回傳同一種 context 的值。
而 (>>=) 則關注在我們 function 可以不用特別拆解 context (monad)也可以將 function apply 到 context 中 。也就是說它會將 function 跟 monad 的值綁定在一起,這也是它叫做 Bind 的原因
import Control.Monad
Just 20 >>= (`divide` 10) -- Just 2
Just 20 >>= (`divide` 0) -- Nothing
Just 20 >>= (`divide` 10) >>= (`divide` 2) -- Just 1
Just 20 >>= (`divide` 0) >>= (`divide` 2) -- Nothing
我們使用了 (>>=)
來幫我們將 Just 20
apply 到只接受 Int
的 divide
。
也就是說我們將 Just 20
與 divide
「bind」在 Monad
中,而且我們也不必手動解開 Monad
然後接下來我們就簡單讓 Monad
的值與「不在 Monad
中的值」進行運算。
那如果沒有 (>>=)
我們的程式碼會怎麼樣呢?會變成我們在做類似於
Just 20 >>= (`divide` 0) >>= (`divide` 2) -- Nothing
這種鏈式的操作實會變得很麻煩
let result =
case 20 `divide` 0 of
Nothing -> Nothing
Just x ->
case x `divide` 2 of
Nothing -> Nothing
y -> y
因為 divide
是會回傳 Maybe Int
所以會看到我們每次運算時都要再用 pattern matching 來檢查現在使否是 Nothing
以及拆開 Just x
然後再進行 divide
運算。
今天的程式碼:
https://github.com/toddLiao469469/30days-for-haskell