《Haskell趣學指南》筆記之 Monad
monad 是加強版的 applicative 函子。
我現在有點忘了什麼是 applicative 函子,所以我先複習一下。
複習
函子(Functor 型別類)
支援 fmap 的型別就是函子,比如 Maybe fmap 的定義是 (a -> b) -> f a -> f b
其意義是把一個容器裡的值,經過一個函式加工一下,然後放回一樣的容器裡。
Applicative 型別類
支援 pure 和 <*>
的型別就是 applicative 的例項,比如 Maybe pure 的定義是 a -> f a
,其作用是把一個值裝進容器裡 <*>
的定義是 f (a -> b) -> f a -> f b
其意義跟 fmap 很相似,區別在於那個函式也在容器裡。
但是有些時候容器比喻並不那麼有用,比如「(->) r 也是 Application 的例項」那裡。
換個角度再說一遍
假設一,我們有
- 型別為 A 的普通值
- 普通函式 A -> B
- 想得到型別為 B 的返回值
那麼用直接呼叫普通函式即可。
假設二,我們有
- 型別為 Maybe A 的奇特值
- 普通函式 A -> B
- 想得到型別為 Maybe B 的返回值
那麼我們就要求 Maybe 滿足 fmap 的條件,然後呼叫 fmap 和普通函式
ghci> fmap (++ "!") (Just "wisdom") Just "wisdom!" ghci> fmap (++ "!") Nothing Nothing 複製程式碼
假設三,我們有
- 型別為 Maybe A 的奇特值
- 奇特函式 Maybe (A -> B)
- 想得到型別為 Maybe B 的返回值
那麼我們就要求 Maybe 滿足 pure 和 <*>
,然後呼叫 <*>
和奇特函式
ghci> Just (+3) *> Just 3 Just 6 ghci> Nothing <*> Just "greed" Nothing ghci> Just ord <*> Nothing Nothing -- pure 的意義是讓普通函式也能用 -- max <$> 等價於 pure max <*> ghci> max <$> Just 3 <*> Just6 Just 6 ghci> max <$> Just 3 <*> Nothing Nothing 複製程式碼
假設四,我們有
- 型別為 Maybe A 的奇特值
- 函式 A -> Maybe B(如果函式是 A -> B,那麼很容易變成 A -> Maybe B,因為 B -> Maybe B 很容易實現)
- 想得到型別為 Maybe B 的返回值
這就是 Monad 要解決的問題。
怎麼做到這一點呢?我們還是以 Maybe 為例子(因為 Maybe 實際上確實是 monad)。
先把問題簡化一下:
- 型別為 A 的普通值
- 函式 A -> Maybe B
- 想得到型別為 Maybe B 的返回值
這就很簡單了,直接呼叫函式即可。
接下來考慮如果把引數從「型別為 A 的普通值」改為「型別為 Maybe A 的奇特值」,我們需要額外做什麼事情。
很顯然,如果引數是 Just A,就取出 A 值呼叫函式;如果引數是 Nothing,就返回 Nothing。
所以實現如下:
applyMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b applyMaybe Nothing f = Nothing applyMaybe (Just x) f = f x 複製程式碼
Haskell 把 applyMaybe 叫做 >>=
。
Monad 型別類的定義
class Monad m where return :: a -> m a-- 跟 pure 一樣 (>>=) :: m a -> (a -> m b) -> m b -- 書上說先不用管下面的 >> 和 fail (>>):: m a -> m b -> m b x >> y =x >>= \_ -> y fail :: String -> m a fail msg = error msg 複製程式碼
理論上一個型別成為 Monad 的例項之前,應該先成為 Applicative 的例項,就如同 class (Functor f) => Applicative f where
一樣。 但是這裡的 class Monad 並沒有出現 (Applicative m) 是為什麼呢?
書上說是因為「在Haskell設計之初,人們沒有考慮到applicative函子會這麼有用」。好吧,我信了。
Maybe 是 Monad 的例項
instance Monad Maybe where return x = Just x Nothing >>= f = Nothing Just x >>= f = f x fail _ = Nothing 複製程式碼
用一下
ghci> return "WHAT" :: Maybe String Just "WHAT" ghci> Just 9 >>= \x -> return (x*10) -- 注意 return 不是退出 Just 90 ghci> Nothing >>= \x -> return (x*10) Nothing 複製程式碼
Monad 有什麼意義?
上文說道
假設四,我們有
- 型別為 Maybe A 的奇特值
- 函式 A -> Maybe B
- 想得到型別為 Maybe B 的返回值
這就是 Monad 要解決的問題。
那我們為什麼要研究這個問題?
書上舉了一個例子,我這裡簡述一下。
引數 Maybe A 有兩個可能,一是 Just A,而是 Nothing。
我們把 Just A 看成是成功的 A,把 Nothing 看成是失敗。
那麼函式 A -> Maybe B 就是能夠對成功的 A 進行處理的一個函式,它的返回值是成功的 B 或者失敗。
如果還有一個函式 B -> Maybe C,就可以繼續處理成功的 B 了。
這很像 Promise !
- 引數為
Promise<User>
- 函式為
User -> Promise<Role>
(大部分時候我們的函式是 User -> Role,但是要把 Role 變成Promise<Role>
很簡單,Promise.resolve 就能做到) - 然後我們就可以把上面兩個東西連起來,得到
Promsie<Role>
了!
但是注意我沒有說 Promise 就是 Monad,我目前還不知道到底是不是。
對應的 Haskell 程式碼就像這樣
return A >>= AToMaybeB >>= BToMaybeC >>= CToMaybeD 複製程式碼
對應的 JS 程式碼
PromiseUser.then(UserToRole).then(RoleToElse) 複製程式碼
do 語法
普通函式裡有
let x=3; y="!" in show x ++ y 複製程式碼
如果把 x 和 y 都放到 Maybe 裡,然後用 >>=
連起來,是這樣
Just 3 >>= (\x -> Just "!" >>= (\y -> Just (show x ++ y ))) 複製程式碼
為了免去這種麻煩的寫法,我們可以用 do
foo :: Maybe String foo = do x <- Just 3 y <- Just "!" Just (show x++y ) 複製程式碼
所以 do 只是把 monad 值串起來的語法罷了。(沒錯 IO 也是 monad)
do 要求裡面每一行 <-
的右邊都是一個 monad 值。上例中每一行都是一個 Maybe 值。
模式匹配與 fail
justH :: Maybe Char justH = do (x:xs) <- Just "hello" return x 複製程式碼
最終 justH 的值是 Just 'h'。 但是如果模式匹配失敗了會怎麼辦?
wopwop :: Maybe Char wopwop = do (x:xs) <- Just "" return x 複製程式碼
"" 是一個空的列表,沒有辦法得到 x,那麼就會呼叫 monal 的 fail,fail 的預設實現是
fail :: (Monad m) => String -> m a fail msg = error msg -- 但是 Maybe 的 fail 是這樣實現的 fail _ = Nothing 複製程式碼
所以如果匹配失敗,會得到 Nothing,以避免程式崩潰,這很巧妙。
列表是 Monad
instance Monad [] where return x= [x] xs >>= f= concat (map f xs) fail _= [] 複製程式碼
使用示例:
ghci> [3,4,5] >>= \x -> [x,x] [3,-3,4,-4,5,-5] ghci> [] >>= \x -> ["bad","mad","rad"] [] ghci> [1,2,3] >>= \x -> [] [] ghci> [1,2] >>= \n -> ['a','b'] >>= \ch -> return(n,ch) [(1,'a'),(1,'b'),(2,'a'),(2,'b')] -- 用 do 改寫 listOfTuples :: [(Int,Char)] listOfTuples = do n<-[1,2] ch<-['a','b'] return (n,ch) 複製程式碼

monad 定律
Haskell 無法檢查一個型別是否滿足 monad 定律,需要開發者自己確保。
- 左單位元律——
return x >>= f
的值必須和f x
一樣 - 右單位元律——
m >>= return
和m
的值必須一樣 - 結合律——
(m >>= f) >>= g
和 m>>= (\x -> f x >>= g)
一樣