什么是Monad
Haskell是一門純函數(shù)式的語言琐旁,純函數(shù)的優(yōu)點(diǎn)是安全可靠缭保。函數(shù)輸出完全取決于輸入喻犁,不存在任何隱式依賴钞啸,它的存在如同數(shù)學(xué)公式般完美無缺皱炉∷ⅲ可是純函數(shù)因?yàn)楦艚^了外部環(huán)境朽缴,連最基本的輸入輸出都無法完成祟蚀。而?Monad?就是 Haskell 給出的解決方案瞎疼。但Monad?并不僅僅是 IO 操作的抽象科乎,它更是多種類似操作之間共性的抽象。所以 Monad 解決的問題并不局限在 IO 上贼急,像 Haskell 中的?Maybe?和?[]?都是?Monad茅茂。Haskell 中漂亮的錯(cuò)誤處理方式,?do?表示法和靈活的列表推導(dǎo)式 (list comprehension) 都算是?Monad?的貢獻(xiàn)太抓。
Monad?基本上是一種加強(qiáng)版的?Applicative Functor空闲,正如?Applicative Functor?是?Functor?的加強(qiáng)版一樣。所以在充分理解?Applicative Functor?的基礎(chǔ)上腻异,過渡到?Monad?其實(shí)是非常平滑的进副。
-- Monad的定義classMonadmwherereturn :: a -> m a? ? (>>=) :: m a -> (a -> m b) -> m b? ? (>>) :: m a -> m b -> m b? ? x >> y = x >>= \_ -> y? ? fail ::String-> m a? ? fail msg = error msg
return?跟其他語言中的?return?是完全不一樣的,它是一個(gè)把普通值包進(jìn)一個(gè) context 里面的函數(shù)悔常,并不是結(jié)束函數(shù)執(zhí)行的關(guān)鍵字影斑。其實(shí)等價(jià)于Applicative中的?pure。
>>?忽略前面表達(dá)式的返回值机打,直接執(zhí)行當(dāng)前表達(dá)式矫户。
>>=?接受一個(gè) monadic value(也就是具有 context 的值,可以用裝有普通值的盒子來比喻)并且把它喂給一個(gè)接受普通值的函數(shù)残邀,并回傳一個(gè) monadic value皆辽。
=<<?和上面?>>=?功能一樣柑蛇,只是結(jié)合順序相反。
Monad 的原理
函數(shù)之間要協(xié)作驱闷,就必須以各種形式交互連接耻台。但如何隔離純函數(shù)與副作用函數(shù),同時(shí)又能讓兩類函數(shù)相互復(fù)用呢空另?
以 IO 操作為例子分析盆耽,為了充分隔離純函數(shù)與 IO 函數(shù),Haskell 中不能實(shí)現(xiàn)?IO Char -> Char?這樣一種輸入是 IO 類型返回值卻是普通類型的函數(shù)扼菠。否則副作用函數(shù)就能很容易變身為純函數(shù)了摄杂。事實(shí)上一旦參數(shù)中有 IO,返回值必有 IO循榆,這就保證了充分隔離析恢。
那如何讓純函數(shù)與 IO 函數(shù)相互復(fù)用呢?這就要靠 IO Monad 中定義的?return?和?>>=?這兩個(gè)函數(shù)了秧饮。return?(在 Haskell 中不是關(guān)鍵字映挂,只是一個(gè)函數(shù)名)的作用是將某個(gè)類型為?A?的值?a?提升(裝箱)為類型為?IO A?的值?Char -> IO Char?。有了這個(gè)函數(shù)后盗尸,純函數(shù)就可以通過?return?變成返回值為 IO 帶副作用的函數(shù)了袖肥。
有了提升而沒有下降操作,怎么復(fù)合?putChar :: Char -> IO()?與?getChar :: IO Char?呢振劳。 getChar 從 IO 讀取一個(gè)字符, putChar 把字符寫入 IO油狂。但?getChar?返回的是?IO Char?類型历恐,而?putChar?需要的是普通的?Char?類型,兩者不匹配怎么辦专筷??>>=?函數(shù)出馬了弱贼!?>>=?的類型是
IOa -> (a ->IOb) ->IOb
這樣?>>=?就可以連接?getChar?與?putChar?,把輸入寫到輸出中
getChar>>= putChar
可以看到?>>=?操作實(shí)際上是類型下降(或拆箱)操作磷蛹,同時(shí)執(zhí)行下降操作的函數(shù)返回值也必須是 IO 類型吮旅。這樣既充分隔離純函數(shù)與副作用函數(shù),又能讓函數(shù)相互復(fù)用味咳。通過?return?和?>>=?兩個(gè)平行世界 (范疇) 就有了可控的交流通道庇勃。
do 表示法
Haskell的 do 表示法實(shí)際上是Monad的語法糖:它給我們提供了一種不使用 (>>=) 和匿名函數(shù)來寫monadic代碼的方式。去除do語法糖的過程就是把它轉(zhuǎn)換為 (>>=) 和匿名函數(shù)槽驶。
do 表示法可以使用分號(hào)?;?和大括號(hào)?{ }?將語句分塊责嚷;但一般會(huì)使用一個(gè)表達(dá)式一行的方式,不同的作用域用不同的縮進(jìn)區(qū)分掂铐。
我們還是以IO 為例子罕拂,接受兩次的鍵盤輸入揍异,然后將兩次輸入的字符串合并成一個(gè)字符串,最后屏幕打印輸出爆班。?>>=?會(huì)接受前面表達(dá)式的值衷掷;>>則會(huì)忽略前面表達(dá)式的值;這里使用?return?實(shí)際它返回的仍然是IO String柿菩,因?yàn)镠askell會(huì)自動(dòng)類型推導(dǎo)得出戚嗅。monadic 的表達(dá)式代碼如下:
(++) <$> getLine <*> getLine >>= print >> return"over"111222>"111222">"over"
使用 do改寫,明顯更加清晰碗旅,和我們熟悉的命令式語言風(fēng)格差不多渡处。
<-?表示從monadic value中取出普通值,可以看成是拆開盒子取出所需要的值祟辟。
foo::IOStringfoo=dox <- getLine? ? y <- getLine? ? print (x ++ y)? ? return"over"
do語法對應(yīng)模式
do{e}? ? ? ? ? ? -> edo{e; es}? ? ? ? -> e >>do{es}do{letdecls; es} ->letdeclsindo{es}do{p <- e; es}? ? -> e >>= \p -> es
Monad 類型
來看一下幾個(gè)默認(rèn)的Monad類型医瘫,它們都必須實(shí)現(xiàn)?return,>>=,fail這幾個(gè)函數(shù)。
Maybe
中間任何一步只要有Nothing旧困,結(jié)果就提前返回Nothing醇份。沒有任何意外的情況才返回Just 值。
-- Maybe 的 Monad instanceinstanceMonadMaybewherereturn x =JustxNothing>>= f =NothingJustx >>= f? = f x? ? fail _ =Nothing-- 實(shí)例Just3>>= (\x ->Nothing>>= (\y ->Just(show x ++ y)))>NothingJust3>>= (\x ->Just"!">>= (\y ->Just(show x ++ y)))>Just"3!"
使用 do 表示法寫成這樣:
foo::MaybeStringfoo=dox <-Just3y <-Just"!"Just(show x ++ y)
List
>>=?基本上就是接受一個(gè)有 context 的值吼具,把他喂進(jìn)一個(gè)只接受普通值的函數(shù)僚纷,并回傳一個(gè)具有 context 的值。[ ]?其實(shí)等價(jià)于 Nothing拗盒。
當(dāng)我們用?>>=?把一個(gè) list 喂給這個(gè)函數(shù)怖竭,lambda 會(huì)映射每個(gè)元素,會(huì)計(jì)算出一串包含一堆 list 的 list陡蝇,最后再把這些 list 壓扁痊臭,得到一層的 list。這就是我們得到 列表?list?處理 Mondic value 的過程登夫。
--list 的 Monad instanceinstanceMonad[]wherereturn x = [x]? ? xs >>= f = concat (map f xs)? ? fail _ = []-- 實(shí)例[3,4,5] >>= \x -> [x,-x]> [3,-3,4,-4,5,-5][1,2,3] >>= \x -> return (-x)> [-1,-2,-3]
list comprehension?也不過是?Monad?的語法糖
[1,2] >>= \n -> ['a','b'] >>= \ch -> return (n,ch)-- Monad[ (n,ch) | n <- [1,2], ch <- ['a','b'] ]-- list comprehension> [(1,'a'),(1,'b'),(2,'a'),(2,'b')]
list comprehension?的過濾基本上跟 guard 是一致的广匙。
[1..50] >>= (\x -> guard ('7' `elem` show x) >> return x)> [7,17,27,37,47]
用?do?改寫, 如果不寫最后一行?return x,那整個(gè) list 就會(huì)是包含一堆空?tuple?的 list恼策。
sevensOnly:: [Int]sevensOnly=dox <- [1..50]? ? guard ('7' `elem` show x)? ? return x-- 對應(yīng)的 list comprehension[ x | x <- [1..50], '7' `elem` show x ]> [7,17,27,37,47]
Either
在?Control.Monad.Error?里面有?Error的?Monad instance鸦致。
instance(Errore) =>Monad(Eithere)wherereturn x =RightxRightx >>= f = f xLefterr >>= f =Lefterr? ? fail msg =Left(strMsg msg)Right3>>= \x -> return (x +100) ::EitherStringInt>Right103
Monad 規(guī)則
return a >>= f == f a
== 左邊的表達(dá)式等價(jià)于右邊的表達(dá)式。如果僅僅是把一個(gè)值包裝到monad里面然后使用 (>>=) 調(diào)用的話涣楷,我們就沒有必要使用?return?分唾;這條規(guī)則對于我們的代碼風(fēng)格有著實(shí)際的指導(dǎo)意義:我們不應(yīng)該寫一些不必要的代碼;這條規(guī)則保證了簡短的寫法和冗余的寫法是等價(jià)的狮斗。
return3>>= (\x ->Just(x+100000))-- 和直接函數(shù)調(diào)用沒有區(qū)別
m >>= return == m
這一條規(guī)則對風(fēng)格也有好處:如果在一系列的action塊里面鳍寂,如果最后一句就是需要返回的正確結(jié)果,那么就不需要使用 return 了情龄;和第一條規(guī)則一樣迄汛,這條規(guī)律也能幫助我們簡化代碼捍壤。
Just"move on up">>= return-- 可以不需要 return
(m >>= f) >>= g == m >>= (\x -> f x >>= g)
當(dāng)我們用 >>= 把一串 monadic function 串在一起,他們的先后順序不應(yīng)該影響結(jié)果鞍爱。
而這不就是結(jié)合律嗎鹃觉?我們可以把那些子action提取出來組合成一個(gè)新action。
(<=<) 可以用來合成兩個(gè) monadic functions, 類似于普通函數(shù)結(jié)合(.)睹逃, 而(>=>) 表示結(jié)合順序相反盗扇。
(<=<) :: (Monadm) => (b -> m c) -> (a -> m b) -> (a -> m c)f<=< g = (\x -> g x >>= f)-- 普通函數(shù)結(jié)合(.)letf = (+1) . (*100)f4>401-- 合成monadic functions (<=<)letg = (\x -> return (x+1)) <=< (\x -> return (x*100))Just4>>= g>Just401-- 也可以將 monadic 函數(shù)用foldr,id 和(.)合成 letf = foldr (.) id [(+1),(*100),(+1)]f1>201
Monad 的 (->) r 形態(tài)
(->) r?不只是一個(gè)?functor?和?applicative functor,同時(shí)也是一個(gè)?monad沉填。
每一個(gè)?monad?都是個(gè)?applicative functor疗隶,而每一個(gè)?applicative functor也都是一個(gè)?functor。盡管?moand?有?functor?跟?applicative functor的性質(zhì)翼闹,但他們不見得有?Functor?跟?Applicative?的 instance 定義斑鼻。
instanceMonad((->) r)wherereturn x = \_ -> x? ? h >>= f = \w -> f (h w) w
Monad 輔助函數(shù)
帶下劃線函數(shù)等價(jià)于不帶下劃線的函數(shù), 只是不返回值
>>= :: m a -> (a -> m b) -> m b=<< :: (a -> m b) -> m a -> m bform:: t a -> (a -> m b) -> m (t b)form_:: t a -> (a -> m b) -> m ()mapM:: (a -> m b) -> t a -> m (t b)mapM_:: (a -> m b) -> t a -> m ()filterM:: (a -> mBool) -> [a] -> m [a]foldM:: (b -> a -> m b) -> b -> t a -> m bsequence:: t (m a) -> m (t a)sequence_:: t (m a) -> m ()liftM:: (a1 -> r) -> m a1 -> m rwhen::Bool-> f () -> f ()join:: m (m a) -> m a
其中在 IO 中經(jīng)常用到的一些函數(shù)
sequence
sequence?接受一串 I/O action,并回傳一個(gè)會(huì)依序執(zhí)行他們的 I/O action猎荠。運(yùn)算的結(jié)果是包在一個(gè) I/O action 的一連串 I/O action 的運(yùn)算結(jié)果坚弱。
main=doa <- getLine? ? b <- getLine? ? c <- getLine? ? print [a,b,c]
其實(shí)可以寫成
main=dors <- sequence [getLine, getLine, getLine]? ? print rs
一個(gè)常見的使用方式是我們將?print?或?putStrLn?之類的函數(shù) map 到串列上。
sequence(map print [1,2,3,4,5])12345[(),(),(),(),()]
mapM?跟?mapM_
由于對一個(gè)串列 map 一個(gè)回傳 I/O action 的函數(shù)关摇,然后再?sequence?這個(gè)動(dòng)作太常用了荒叶。所以函式庫中提供了?mapM?跟?mapM_。mapM接受一個(gè)函數(shù)跟一個(gè)串列输虱,將對串列用函數(shù) map 然后 sequence 結(jié)果些楣。mapM_?也作同樣的事,只是他把運(yùn)算的結(jié)果丟掉而已宪睹。在我們不關(guān)心 I/O action 結(jié)果的情況下戈毒,mapM_?是最常被使用的。
mapMprint [1,2,3]123[(),(),()]mapM_print [1,2,3]123
form?和?form_?與?mapM?和?mapM_?類似横堡,不過只是把列表參數(shù)提前。
還有一些是在?monad?中定義冠桃,且等價(jià)于?functor?或?applicative functor?中所具有的函數(shù)命贴。
liftM
liftM?跟?fmap?等價(jià), 也有?liftM3,liftM4?跟?liftM5
liftM:: (Monadm) => (a -> b) -> m a -> m bliftMf m = m >>= (\x -> return (f x))liftM(*2) [1,2]> [2,4]
ap
ap?基本上就是 **<*>**食听,只是他限制在 Monad 上而不是 Applicative 上胸蛛。
ap:: (Monadm) => m (a -> b) -> m a -> m bapmf m =dof <- mf? ? x <- m? ? return (f x)ap[(*2)] [1,2,3]> [2,4,6]
join
m >>= f?永遠(yuǎn)等價(jià)于?join (fmap f m)?這性質(zhì)非常有用
join:: (Monadm) => m (m a) -> m ajoin(Just(Just9))>Just9join[[1,2,3],[4,5,6]]-- 對于 list 而言 join 不過就是 concat> [1,2,3,4,5,6]
filterM
filterM,除了能做 filter 的動(dòng)作樱报,同時(shí)還能保有 monadic context葬项。
filterM:: (Monadm) => (a -> mBool) -> [a] -> m [a]filterM(\x -> return (x >2)) [1,2,3,4]> [3,4]
foldM
foldl?的 monadic 的版本叫做?foldM
foldM:: (Monadm) => (a -> b -> m a) -> a -> [b] -> m afoldM(\x y -> return (x + y))0[1,2,3]>6