《Haskell趣學指南》筆記之 Monad

monad 是增強版的 applicative 函子。bash

我如今有點忘了什麼是 applicative 函子,因此我先複習一下。app

複習

函子(Functor 類型類)

支持 fmap 的類型就是函子,好比 Maybe fmap 的定義是 (a -> b) -> f a -> f b 其意義是把一個容器裏的值,通過一個函數加工一下,而後放回同樣的容器裏。dom

Applicative 類型類

支持 pure 和 <*> 的類型就是 applicative 的實例,好比 Maybe pure 的定義是 a -> f a,其做用是把一個值裝進容器裏 <*> 的定義是 f (a -> b) -> f a -> f b 其意義跟 fmap 很類似,區別在於那個函數也在容器裏。函數

可是有些時候容器比喻並不那麼有用,好比「(->) r 也是 Application 的實例」那裏。spa

換個角度再說一遍

假設一,咱們有設計

  • 類型爲 A 的普通值
  • 普通函數 A -> B
  • 想獲得類型爲 B 的返回值

那麼用直接調用普通函數便可。code

假設二,咱們有cdn

  • 類型爲 Maybe A 的奇特值
  • 普通函數 A -> B
  • 想獲得類型爲 Maybe B 的返回值

那麼咱們就要求 Maybe 知足 fmap 的條件,而後調用 fmap 和普通函數blog

ghci> fmap (++ "!") (Just "wisdom")
Just "wisdom!"
ghci> fmap (++ "!") Nothing
Nothing
複製代碼

假設三,咱們有ci

  • 類型爲 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 定律,須要開發者本身確保。

  1. 左單位元律——return x >>= f 的值必須和 f x 同樣
  2. 右單位元律——m >>= returnm 的值必須同樣
  3. 結合律——(m >>= f) >>= g 和 m >>= (\x -> f x >>= g) 同樣
相關文章
相關標籤/搜索