monad 是增強版的 applicative 函子。bash
我如今有點忘了什麼是 applicative 函子,因此我先複習一下。app
支持 fmap 的類型就是函子,好比 Maybe fmap 的定義是 (a -> b) -> f a -> f b
其意義是把一個容器裏的值,通過一個函數加工一下,而後放回同樣的容器裏。dom
支持 pure 和 <*>
的類型就是 applicative 的實例,好比 Maybe pure 的定義是 a -> f a
,其做用是把一個值裝進容器裏 <*>
的定義是 f (a -> b) -> f a -> f b
其意義跟 fmap 很類似,區別在於那個函數也在容器裏。函數
可是有些時候容器比喻並不那麼有用,好比「(->) r 也是 Application 的實例」那裏。spa
假設一,咱們有設計
那麼用直接調用普通函數便可。code
假設二,咱們有cdn
那麼咱們就要求 Maybe 知足 fmap 的條件,而後調用 fmap 和普通函數blog
ghci> fmap (++ "!") (Just "wisdom")
Just "wisdom!"
ghci> fmap (++ "!") Nothing
Nothing
複製代碼
假設三,咱們有ci
那麼咱們就要求 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
複製代碼
假設四,咱們有
這就是 Monad 要解決的問題。
怎麼作到這一點呢?咱們仍是以 Maybe 爲例子(由於 Maybe 實際上確實是 monad)。
先把問題簡化一下:
這就很簡單了,直接調用函數便可。
接下來考慮若是把參數從「類型爲 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 叫作 >>=
。
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函子會這麼有用」。好吧,我信了。
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
複製代碼
上文說道
假設四,咱們有
- 類型爲 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)
複製代碼
普通函數裏有
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 值。
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,以免程序崩潰,這很巧妙。
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)
複製代碼
Haskell 沒法檢查一個類型是否知足 monad 定律,須要開發者本身確保。
return x >>= f
的值必須和 f x
同樣m >>= return
和 m
的值必須同樣(m >>= f) >>= g
和 m >>= (\x -> f x >>= g)
同樣