第十二章 Haskell第一次接觸Monad

2022-08-08 14:29 更新

來看看幾種 Monad

當(dāng)我們第一次談到 Functor 的時(shí)候,我們了解到他是一個(gè)抽象概念,代表是一種可以被 map over 的值。然后我們?cè)賹⑵涓拍钐嵘?Applicative Functor,他代表一種帶有 context 的型態(tài),我們可以用函數(shù)操作他而且同時(shí)還保有他的 context。

在這一章,我們會(huì)學(xué)到 Monad,基本上他是一種加強(qiáng)版的 Applicative Functor,正如 Applicative Functor 是 Functor 的加強(qiáng)版一樣。

我們介紹到 Functor 是因?yàn)槲覀冇^察到有許多型態(tài)都可以被 function 給 map over,了解到這個(gè)目的,便抽象化了 Functor 這個(gè) typeclass 出來。但這讓我們想問:如果給定一個(gè) a -> b 的函數(shù)以及 f a 的型態(tài),我們要如何將函數(shù) map over 這個(gè)型態(tài)而得到 f b?我們知道要如何 map over Maybe a,[a] 以及 IO a。我們甚至還知道如何用 a -> b map over r -> a,并且會(huì)得到 r -> b。要回答這個(gè)問題,我們只需要看 fmap 的型態(tài)就好了:

fmap :: (Functor f) => (a -> b) -> f a -> f b      

然后只要針對(duì) Functor instance 撰寫對(duì)應(yīng)的實(shí)作。

之后我們又看到一些可以針對(duì) Functor 改進(jìn)的地方,例如 a -> b 也被包在一個(gè) Functor value 里面呢?像是 Just (*3),我們要如何 apply Just 5 給他?如果我們不要 apply Just 5 而是 Nothing 呢?甚至給定 [(*2),(+4)],我們要如何 apply 他們到 [1,2,3] 呢?對(duì)于此,我們抽象出 Applicative typeclass,這就是我們想要問的問題:

(<*>) :: (Applicative f) => f (a -> b) -> f a -> f b     

我們也看到我們可以將一個(gè)正常的值包在一個(gè)數(shù)據(jù)型態(tài)中。例如說我們可以拿一個(gè) 1 然后把他包成 Just 1?;蚴前阉?nbsp;[1]。也可以是一個(gè) I/O action 會(huì)產(chǎn)生一個(gè) 1。這樣包裝的 function 我們叫他做 pure。

如我們說得,一個(gè) applicative value 可以被看作一個(gè)有附加 context 的值。例如說,'a' 只是一個(gè)普通的字符,但 Just 'a' 是一個(gè)附加了 context 的字符。他不是 Char 而是 Maybe Char,這型態(tài)告訴我們這個(gè)值可能是一個(gè)字符,也可能什么都沒有。

來看看 Applicative typeclass 怎樣讓我們用普通的 function 操作他們,同時(shí)還保有 context:

ghci> (*) <$> Just 2 <*> Just 8  
Just 16  
ghci> (++) <$> Just "klingon" <*> Nothing  
Nothing  
ghci> (-) <$> [3,4] <*> [1,2,3]  
[2,1,0,3,2,1]  

所以我們可以視他們?yōu)?applicative values,Maybe a 代表可能會(huì)失敗的 computation,[a] 代表同時(shí)有好多結(jié)果的 computation (non-deterministic computation),而 IO a 代表會(huì)有 side-effects 的 computation。

Monad 是一個(gè)從 Applicative functors 很自然的一個(gè)演進(jìn)結(jié)果。對(duì)于他們我們主要考量的點(diǎn)是:如果你有一個(gè)具有 context 的值 m a,你能如何把他丟進(jìn)一個(gè)只接受普通值 a 的函數(shù)中,并回傳一個(gè)具有 context 的值?也就是說,你如何套用一個(gè)型態(tài)為 a -> m b 的函數(shù)至 m a?基本上,我們要求的函數(shù)是:

(>>=) :: (Monad m) => m a -> (a -> m b) -> m b

如果我們有一個(gè)漂亮的值跟一個(gè)函數(shù)接受普通的值但回傳漂亮的值,那我們要如何要把漂亮的值丟進(jìn)函數(shù)中?這就是我們使用 Monad 時(shí)所要考量的事情。我們不寫成 f a 而寫成 m a 是因?yàn)?nbsp;m 代表的是 Monad,但 monad 不過就是支持 >>= 操作的 applicative functors。>>= 我們稱呼他為 bind。

當(dāng)我們有一個(gè)普通值 a 跟一個(gè)普通函數(shù) a -> b,要套用函數(shù)是一件很簡(jiǎn)單的事。但當(dāng)你在處理具有 context 的值時(shí),就需要多考慮些東西,要如何把漂亮的值喂進(jìn)函數(shù)中,并如何考慮他們的行為,但你將會(huì)了解到他們其實(shí)不難。

動(dòng)手做做看: Maybe Monad

現(xiàn)在對(duì)于什么是 Monad 已經(jīng)有了些模糊的概念, 我們來看看要如何讓這概念更具體一些。

不意外地,Maybe 是一個(gè) Monad, 所以讓我們對(duì)于他多探討些,看看是否能跟我們所知的 Monad 概念結(jié)合起來。

到這邊要確定你了解什么是 Applicatives。如果你知道好幾種 ``Applicative`` 的 instance 還有他們代表的意含就更好了,因?yàn)?monad 不過就是對(duì) applicative 的概念進(jìn)行一次升級(jí)。

一個(gè) Maybe a 型態(tài)的值代表型態(tài)為 a 的值而且具備一個(gè)可能造成錯(cuò)誤的 context。而 Just "dharma" 的值代表他不是一個(gè) "dharma" 的字串就是字串不見時(shí)的 Nothing。如果你把字串當(dāng)作計(jì)算的結(jié)果,Nothing 就代表計(jì)算失敗了。

當(dāng)我們把 Maybe 視作 functor,我們其實(shí)要的是一個(gè) fmap 來把一個(gè)函數(shù)針對(duì)其中的元素做套用。他會(huì)對(duì) Just 中的元素進(jìn)行套用,要不然就是保留 Nothing 的狀態(tài),其代表里面根本沒有元素。

ghci> fmap (++"!") (Just "wisdom")  
Just "wisdom!"  
ghci> fmap (++"!") Nothing  
Nothing  

或者視為一個(gè) applicative functor,他也有類似的作用。只是 applicative 也把函數(shù)包了起來。Maybe 作為一個(gè) applicative functor,我們能用 <*> 來套用一個(gè)存在 Maybe 中的函數(shù)至包在另外一個(gè) Maybe 中的值。他們都必須是包在 Just 來代表值存在,要不然其實(shí)就是 Nothing。當(dāng)你在想套用函數(shù)到值上面的時(shí)候,缺少了函數(shù)或是值都會(huì)造成錯(cuò)誤,所以這樣做是很合理的。

ghci> Just (+3) <*> Just 3  
Just 6  
ghci> Nothing <*> Just "greed"  
Nothing  
ghci> Just ord <*> Nothing  
Nothing  

當(dāng)我們用 applicative 的方式套用函數(shù)至 Maybe 型態(tài)的值時(shí),就跟上面描述的差不多。過程中所有值都必須是 Just,要不然結(jié)果一定會(huì)是 Nothing。

ghci> max <$> Just 3 <*> Just 6  
Just 6  
ghci> max <$> Just 3 <*> Nothing  
Nothing  

我們來思考一下要怎么為 Maybe 實(shí)作 >>=。正如我們之前提到的,>>= 接受一個(gè) monadic value,以及一個(gè)接受普通值的函數(shù),這函數(shù)會(huì)回傳一個(gè) monadic value。>>= 會(huì)幫我們套用這個(gè)函數(shù)到這個(gè) monadic value。在函數(shù)只接受普通值的情況俠,函數(shù)是如何作到這件事的呢?要作到這件事,他必須要考慮到 monadic value 的 context。

在這個(gè)案例中,>>= 會(huì)接受一個(gè) Maybe a 以及一個(gè)型態(tài)為 a -> Maybe b 的函數(shù)。他會(huì)套用函數(shù)到 Maybe a。要厘清他怎么作到的,首先我們注意到 Maybe 的 applicative functor 特性。假設(shè)我們有一個(gè)函數(shù) \x -> Just (x+1)。他接受一個(gè)數(shù)字,把他加 1 后再包回 Just。

ghci> (\x -> Just (x+1)) 1  
Just 2  
ghci> (\x -> Just (x+1)) 100  
Just 101 

如果我們喂給函數(shù) 1,他會(huì)計(jì)算成 Just 2。如果我們喂給函數(shù) 100,那結(jié)果便是 Just 101。但假如我們喂一個(gè) Maybe 的值給函數(shù)呢?如果我們把 Maybe 想成一個(gè) applicative functor,那答案便很清楚。如果我們拿到一個(gè) Just,就把包在 Just 里面的值喂給函數(shù)。如果我們拿到一個(gè) Nothing,我們就說結(jié)果是 Nothing。

我們調(diào)用 applyMaybe 而不調(diào)用 >>=。他接受 Maybe a 跟一個(gè)回傳 Maybe b 的函數(shù),并套用函數(shù)至 Maybe a。

applyMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b  
applyMaybe Nothing f  = Nothing  
applyMaybe (Just x) f = f x  

我們套用一個(gè) infix 函數(shù),這樣 Maybe 的值可以寫在左邊且函數(shù)是在右邊:

ghci> Just 3 `applyMaybe` \x -> Just (x+1)  
Just 4  
ghci> Just "smile" `applyMaybe` \x -> Just (x ++ " :")""  
Just "smile :""  
ghci> Nothing `applyMaybe` \x -> Just (x+1)  
Nothing  
ghci> Nothing `applyMaybe` \x -> Just (x ++ " :")")  
Nothing 

在上述的范例中,我們看到在套用 applyMaybe 的時(shí)候,函數(shù)是套用在 Just 里面的值。當(dāng)我們?cè)噲D套用到 Nothing,那整個(gè)結(jié)果便是 Nothing。假如函數(shù)回傳 Nothing 呢?

ghci> Just 3 `applyMaybe` \x -> if x > 2 then Just x else Nothing  
Just 3  
ghci> Just 1 `applyMaybe` \x -> if x > 2 then Just x else Nothing  
Nothing  

這正是我們期待的結(jié)果。如果左邊的 monadic value 是 Nothing,那整個(gè)結(jié)果就是 Nothing。如果右邊的函數(shù)是 Nothing,那結(jié)果也會(huì)是 Nothing。這跟我們之前把 Maybe 當(dāng)作 applicative 時(shí),過程中有任何一個(gè) Nothing 整個(gè)結(jié)果就會(huì)是 Nothing 一樣。

對(duì)于 Maybe 而言,我們已經(jīng)找到一個(gè)方法處理漂亮值的方式。我們作到這件事的同時(shí),也保留了 Maybe 代表可能造成錯(cuò)誤的計(jì)算的意義。

你可能會(huì)問,這樣的結(jié)果有用嗎?由于 applicative functors 讓我們可以拿一個(gè)接受普通值的函數(shù),并讓他可以操作具有 context 的值,這樣看起來 applicative functors 好像比 monad 強(qiáng)。但我們會(huì)看到 monad 也能作到,因?yàn)樗皇?applicative functors 的升級(jí)版。他們同時(shí)也能作到 applicative functors 不能作到的事情。

稍候我們會(huì)再繼續(xù)探討 Maybe,但我們先來看看 monad 的 type class。

Monad type class

正如 functors 有 Functor 這個(gè) type class,而 applicative functors 有一個(gè) Applicative 這個(gè) type class,monad 也有他自己的 type class:Monad 他看起來像這樣:

class Monad m where  
    return :: 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  

我們從第一行開始看。他說 class Monad m where。但我們之前不是提到 monad 是 applicative functors 的加強(qiáng)版嗎?不是應(yīng)該有一個(gè)限制說一個(gè)型態(tài)必須先是一個(gè) applicative functor 才可能是一個(gè) monad 嗎?像是 class (Applicative m) = > Monad m where。他的確應(yīng)該要有,但當(dāng) Haskell 被創(chuàng)造的早期,人們沒有想到 applicative functor 適合被放進(jìn)語言中,所以最后沒有這個(gè)限制。但的確每個(gè) monad 都是 applicative functor,即使 Monad 并沒有這么宣告。

在 Monad typeclass 中定義的第一個(gè)函數(shù)是 return。他其實(shí)等價(jià)于 pure,只是名字不同罷了。他的型態(tài)是 (Monad m) => a -> m a。他接受一個(gè)普通值并把他放進(jìn)一個(gè)最小的 context 中。也就是說他把普通值包進(jìn)一個(gè) monad 里面。他跟 Applicative 里面 pure 函數(shù)做的事情一樣,所以說其實(shí)我們已經(jīng)認(rèn)識(shí)了 return。我們已經(jīng)用過 return 來處理一些 I/O。我們用他來做一些假的 I/O,印出一些值。對(duì)于 Maybe 來說他就是接受一個(gè)普通值然后包進(jìn) Just。

提醒一下:``return`` 跟其他語言中的 ``return`` 是完全不一樣的。他并不是結(jié)束一個(gè)函數(shù)的執(zhí)行,他只不過是把一個(gè)普通值包進(jìn)一個(gè) context 里面。

接下來定義的函數(shù)是 bind: >>=。他就像是函數(shù)套用一樣,只差在他不接受普通值,他是接受一個(gè) monadic value(也就是具有 context 的值)并且把他喂給一個(gè)接受普通值的函數(shù),并回傳一個(gè) monadic value。

接下來,我們定義了 >>。我們不會(huì)介紹他,因?yàn)樗幸粋€(gè)事先定義好的實(shí)作,基本上我們?cè)趯?shí)作 Monad typeclass 的時(shí)候都不會(huì)去理他。

最后一個(gè)函數(shù)是 fail。我們通常在我們程序中不會(huì)具體寫出來。他是被 Haskell 用在處理語法錯(cuò)誤的情況。我們目前不需要太在意 fail。

我們知道了 Monad typeclass 長(zhǎng)什么樣子,我們來看一下 Maybe 的 Monad instance。

instance Monad Maybe where  
    return x = Just x  
    Nothing >>= f = Nothing  
    Just x >>= f  = f x  
    fail _ = Nothing  

return跟pure是等價(jià)的。這沒什么困難的。我們跟我們?cè)诙xApplicative的時(shí)候做一樣的事,只是把他用Just包起來。

>>=跟我們的applyMaybe是一樣的。當(dāng)我們將Maybe a塞給我們的函數(shù),我們保留住context,并且在輸入是Nothing的時(shí)候回傳Nothing。畢竟當(dāng)沒有值的時(shí)候套用我們的函數(shù)是沒有意義的。當(dāng)輸入是Just的時(shí)候則套用f并將他包在Just里面。

我們可以試著感覺一下Maybe是怎樣表現(xiàn)成Monad的。

ghci> return "WHAT" :: Maybe String  
Just "WHAT"  
ghci> Just 9 >>= \x -> return (x*10)  
Just 90  
ghci> Nothing >>= \x -> return (x*10)  
Nothing 

第一行沒什么了不起,我們已經(jīng)知道 return 就是 pure 而我們又對(duì) Maybe 操作過 pure 了。至于下兩行就比較有趣點(diǎn)。

留意我們是如何把 Just 9 喂給 \x -> return (x*10)。在函數(shù)中 x 綁定到 9。他看起好像我們能不用 pattern matching 的方式就從 Maybe 中抽取出值。但我們并沒有喪失掉 Maybe 的 context,當(dāng)他是 Nothing 的時(shí)候,>>= 的結(jié)果也會(huì)是 Nothing。

走鋼索

我們已經(jīng)知道要如何把 Maybe a 喂進(jìn) a -> Maybe b 這樣的函數(shù)。我們可以看看我們?nèi)绾沃貜?fù)使用 >>= 來處理多個(gè) Maybe a 的值。

首先來說個(gè)小故事。皮爾斯決定要辭掉他的工作改行試著走鋼索。他對(duì)走鋼索蠻在行的,不過仍有個(gè)小問題。就是鳥會(huì)停在他拿的平衡竿上。他們會(huì)飛過來停一小會(huì)兒,然后再飛走。這樣的情況在兩邊的鳥的數(shù)量一樣時(shí)并不是個(gè)太大的問題。但有時(shí)候,所有的鳥都會(huì)想要停在同一邊,皮爾斯就失去了平衡,就會(huì)讓他從鋼索上掉下去。

我們這邊假設(shè)兩邊的鳥差異在三個(gè)之內(nèi)的時(shí)候,皮爾斯仍能保持平衡。所以如果是右邊有一只,左邊有四只的話,那還撐得住。但如果左邊有五只,那就會(huì)失去平衡。

我們要寫個(gè)程序來仿真整個(gè)情況。我們想看看皮爾斯究竟在好幾只鳥來來去去后是否還能撐住。例如說,我們想看看先來了一只鳥停在左邊,然后來了四只停在右邊,然后左邊那只飛走了。之后會(huì)是什么情形。

我們用一對(duì)整數(shù)來代表我們的平衡竿狀態(tài)。頭一個(gè)位置代表左邊的鳥的數(shù)量,第二個(gè)位置代表右邊的鳥的數(shù)量。

type Birds = Int  
type Pole = (Birds,Birds)  

由于我們用整數(shù)來代表有多少只鳥,我們便先來定義 Int 的同義型態(tài),叫做 Birds。然后我們把 (Birds, Birds) 定義成 Pole。

接下來,我們定義一個(gè)函數(shù)他接受一個(gè)數(shù)字,然后把他放在竿子的左邊,還有另外一個(gè)函數(shù)放在右邊。

landLeft :: Birds -> Pole -> Pole  
landLeft n (left,right) = (left + n,right)  
  
landRight :: Birds -> Pole -> Pole  
landRight n (left,right) = (left,right + n)  

我們來試著執(zhí)行看看:

ghci> landLeft 2 (0,0)  
(2,0)  
ghci> landRight 1 (1,2)  
(1,3)  
ghci> landRight (-1) (1,2)  
(1,1)  

要仿真鳥飛走的話我們只要給定一個(gè)負(fù)數(shù)就好了。 由于這些操作是接受 Pole 并回傳 Pole, 所以我們可以把函數(shù)串在一起。

ghci> landLeft 2 (landRight 1 (landLeft 1 (0,0)))  
(3,1)

當(dāng)我們喂 (0,0) 給 landLeft 1 時(shí),我們會(huì)得到 (1,0)。接著我們仿真右邊又停了一只鳥,狀態(tài)就變成 (1,1)。最后又有兩只鳥停在左邊,狀態(tài)變成 (3,1)。我們這邊的寫法是先寫函數(shù)名稱,然后再套用參數(shù)。但如果先寫 pole 再寫函數(shù)名稱會(huì)比較清楚,所以我們會(huì)想定義一個(gè)函數(shù)

x -: f = f x

我們能先套用參數(shù)然后再寫函數(shù)名稱:

ghci> 100 -: (*3)  
300  
ghci> True -: not  
False  
ghci> (0,0) -: landLeft 2  
(2,0)  

有了這個(gè)函數(shù),我們便能寫得比較好讀一些:

ghci> (0,0) -: landLeft 1 -: landRight 1 -: landLeft 2  
(3,1)  

這個(gè)范例跟先前的范例是等價(jià)的,只不過好讀許多。很清楚的看出我們是從 (0,0) 開始,然后停了一只在左邊,接著右邊又有一只,最后左邊多了兩只。

到目前為止沒什么問題,但如果我們要停 10 只在左邊呢?

ghci> landLeft 10 (0,3)  
(10,3)  

你說左邊有 10 只右邊卻只有 3 只?那不是早就應(yīng)該掉下去了?這個(gè)例子太明顯了,如果換個(gè)比較不明顯的例子。

ghci> (0,0) -: landLeft 1 -: landRight 4 -: landLeft (-1) -: landRight (-2)  
(0,2)  

表面看起來沒什么問題,但如果你仔細(xì)看的話,有一瞬間是右邊有四只,但左邊沒有鳥。要修正這個(gè)錯(cuò)誤,我們要重新查看 landLeft 跟 landRight。我們其實(shí)是希望這些函數(shù)產(chǎn)生失敗的情況。那就是在維持平衡的時(shí)候回傳新的 pole,但失敗的時(shí)候告訴我們失敗了。這時(shí)候 Maybe 就剛剛好是我們要的 context 了。我們用 Maybe 重新寫一次:

landLeft :: Birds -> Pole -> Maybe Pole  
landLeft n (left,right)  
    | abs ((left + n) - right) < 4 = Just (left + n, right)  
    | otherwise                    = Nothing  
          
landRight :: Birds -> Pole -> Maybe Pole  
landRight n (left,right)  
    | abs (left - (right + n)) < 4 = Just (left, right + n)  
    | otherwise                    = Nothing  

現(xiàn)在這些函數(shù)不回傳 Pole 而回傳 Maybe Pole 了。他們?nèi)越邮茗B的數(shù)量跟舊的的 pole,但他們現(xiàn)在會(huì)檢查是否有太多鳥會(huì)造成皮爾斯失去平衡。我們用 guards 來檢查是否有差異超過三的情況。如果沒有,那就包一個(gè)在 Just 中的新的 pole,如果是,那就回傳 Nothing。

再來執(zhí)行看看:

ghci> landLeft 2 (0,0)  
Just (2,0)  
ghci> landLeft 10 (0,3)  
Nothing  

一如預(yù)期,當(dāng)皮爾斯不會(huì)掉下去的時(shí)候,我們就得到一個(gè)包在 Just 中的新 pole。當(dāng)太多鳥停在同一邊的時(shí)候,我們就會(huì)拿到 Nothing。這樣很棒,但我們卻不知道怎么把東西串在一起了。我們不能做 landLeft 1 (landRight 1 (0,0)),因?yàn)楫?dāng)我們對(duì) (0,0) 使用 landRight 1 時(shí),我們不是拿到 Pole 而是拿到 Maybe Pole。landLeft 1 會(huì)拿到 Pole 而不是拿到 Maybe Pole。

我們需要一種方法可以把拿到的 Maybe Pole 塞到拿 Pole 的函數(shù)中,然后回傳 Maybe Pole。而我們有 >>=,他對(duì) Maybe 做的事就是我們要的

ghci> landRight 1 (0,0) >>= landLeft 2  
Just (2,1)  

landLeft 2 的型態(tài)是 Pole -> Maybe Pole。我們不能喂給他 Maybe Pole 的東西。而 landRight 1 (0,0) 的結(jié)果就是 Maybe Pole,所以我們用 >>= 來接受一個(gè)有 context 的值然后拿給 landLeft 2。>>= 的確讓我們把 Maybe 當(dāng)作有 context 的值,因?yàn)楫?dāng)我們丟 Nothing 給 landLeft 2 的時(shí)候,結(jié)果會(huì)是 Nothing。

ghci> Nothing >>= landLeft 2  
Nothing  

這樣我們可以把這些新寫的用 >>= 串在一起。讓 monadic value 可以喂進(jìn)只吃普通值的函數(shù)。

來看看些例子:

ghci> return (0,0) >>= landRight 2 >>= landLeft 2 >>= landRight 2  
Just (2,4)  

我們最開始用 return 回傳一個(gè) pole 并把他包在 Just 里面。我們可以像往常套用 landRight 2,不過我們不那么做,我們改用 >>=。Just (0,0) 被喂到 landRight 2,得到 Just (0,2)。接著被喂到 landLeft 2,得到 Just (2,2)。

還記得我們之前引入失敗情況的例子嗎?

ghci> (0,0) -: landLeft 1 -: landRight 4 -: landLeft (-1) -: landRight (-2)  
(0,2)  

之前的例子并不會(huì)反應(yīng)失敗的情況。但如果我們用 >>= 的話就可以得到失敗的結(jié)果。

ghci> return (0,0) >>= landLeft 1 >>= landRight 4 >>= landLeft (-1) >>= landRight (-2)  
Nothing  

正如預(yù)期的,最后的情形代表了失敗的情況。我們?cè)龠M(jìn)一步看看這是怎么產(chǎn)生的。首先 return 把 (0,0) 放到一個(gè)最小的 context 中,得到 Just (0,0)。然后是 Just (0.0) >>= landLeft 1。由于 Just (0,0) 是一個(gè) Just 的值。landLeft 1 被套用至 (0,0) 而得到 Just (1,0)。這反應(yīng)了我們?nèi)员3衷谄胶獾臓顟B(tài)。接著是 Just (1,0) >>= landright 4 而得到了 Just (1,4)。距離不平衡只有一步之遙了。他又被喂給 landLeft (-1),這組合成了 landLeft (-1) (1,4)。由于失去了平衡,我們變得到了 Nothing。而我們把 Nothing 喂給 landRight (-2),由于他是 Nothing,也就自動(dòng)得到了 Nothing。

如果只把 Maybe 當(dāng)作 applicative 用的話是沒有辦法達(dá)到我們要的效果的。你試著做一遍就會(huì)卡住。因?yàn)?applicative functor 并不允許 applicative value 之間有彈性的交互。他們最多就是讓我們可以用 applicative style 來傳遞參數(shù)給函數(shù)。applicative operators 能拿到他們的結(jié)果并把他用 applicative 的方式喂給另一個(gè)函數(shù),并把最終的 applicative 值放在一起。但在每一步之間并沒有太多允許我們作手腳的機(jī)會(huì)。而我們的范例需要每一步都倚賴前一步的結(jié)果。當(dāng)每一只鳥降落的時(shí)候,我們都會(huì)把前一步的結(jié)果拿出來看看。好知道結(jié)果到底應(yīng)該成功或失敗。

我們也能寫出一個(gè)函數(shù),完全不管現(xiàn)在究竟有幾只鳥停在竿子上,只是要害皮爾斯滑倒。我們可以稱呼這個(gè)函數(shù)叫做 banana:

banana :: Pole -> Maybe Pole  
banana _ = Nothing  

現(xiàn)在我們能把香蕉皮串到我們的過程中。他絕對(duì)會(huì)讓遇到的人滑倒。他完全不管前面的狀態(tài)是什么都會(huì)產(chǎn)生失敗。

ghci> return (0,0) >>= landLeft 1 >>= banana >>= landRight 1  
Nothing  

Just (1,0) 被喂給 banana,而產(chǎn)生了 Nothing,之后所有的結(jié)果便都是 Nothing 了。

要同樣表示這種忽略前面的結(jié)果,只注重眼前的 monadic value 的情況,其實(shí)我們可以用 >> 來表達(dá)。

(>>) :: (Monad m) => m a -> m b -> m b  
m >> n = m >>= \_ -> n  

一般來講,碰到一個(gè)完全忽略前面狀態(tài)的函數(shù),他就應(yīng)該只會(huì)回傳他想回傳的值而已。但碰到 Monad,他們的 context 還是必須要被考慮到。來看一下 >> 串接 Maybe 的情況。

ghci> Nothing >> Just 3  
Nothing  
ghci> Just 3 >> Just 4  
Just 4  
ghci> Just 3 >> Nothing  
Nothing  

如果你把 >> 換成 >>= \_ ->,那就很容易看出他的意思。

我們也可以把 banana 改用 >> 跟 Nothing 來表達(dá):

ghci> return (0,0) >>= landLeft 1 >> Nothing >>= landRight 1  
Nothing 

我們得到了保證的失敗。

我們也可以看看假如我們故意不用把 Maybe 視為有 context 的值的寫法。他會(huì)長(zhǎng)得像這樣:

routine :: Maybe Pole  
routine = case landLeft 1 (0,0) of  
    Nothing -> Nothing  
    Just pole1 -> case landRight 4 pole1 of   
            Nothing -> Nothing  
            Just pole2 -> case landLeft 2 pole2 of  
                    Nothing -> Nothing  
                    Just pole3 -> landLeft 1 pole3  

左邊先停了一只鳥,然后我們停下來檢查有沒有失敗。當(dāng)失敗的時(shí)候我們回傳 Nothing。當(dāng)成功的時(shí)候,我們?cè)谟疫呁R恢圾B,然后再重復(fù)前面做的事情。把這些瑣事轉(zhuǎn)換成 >>= 證明了 Maybe Monad 的力量,可以省去我們不少的時(shí)間。

注意到 Maybe 對(duì) >>= 的實(shí)作,他其實(shí)就是在做碰到 Nothing 就會(huì)傳 Nothing,碰到正確值就繼續(xù)用 Just 傳遞值。

在這個(gè)章節(jié)中,我們看過了好幾個(gè)函數(shù),也見識(shí)了用 Maybe monad 來表示失敗的 context 的力量。把普通的函數(shù)套用換成了 >>=,讓我們可以輕松地應(yīng)付可能會(huì)失敗的情況,并幫我們傳遞 context。這邊的 context 就代表失敗的可能性,當(dāng)我們套用函數(shù)到 context 的時(shí)候,就代表考慮進(jìn)了失敗的情況。

do 表示法

Monad 在 Haskell 中是十分重要的,所以我們還特別為了操作他設(shè)置了特別的語法:do 表示法。我們?cè)诮榻B I/O 的時(shí)候已經(jīng)用過 do 來把小的 I/O action 串在一起了。其實(shí) do 并不只是可以用在 IO,他可以用在任何 monad 上。他的原則是簡(jiǎn)單明了,把 monadic value 串成一串。我們這邊來細(xì)看 do 是如何使用,以及為什么我們十分倚賴他。

來看一下熟悉的例子:

ghci> Just 3 >>= (\x -> Just (show x ++ "!"))  
Just "3!"  

你說這沒什么了不起,不過就是把 monadic value 喂給一個(gè)函數(shù)罷了。其中 x 就指定成 3。也從 monadic value 變成了普通值。那如果我們要在 lambda 中使用 >>= 呢?

ghci> Just 3 >>= (\x -> Just "!" >>= (\y -> Just (show x ++ y)))  
Just "3!"  

我們嵌一個(gè) >>= 在另外一個(gè) >>= 中。在外層的 lambda,我們把 Just "!" 喂給 \y -> Just (show x ++ y)。在內(nèi)層的 lambda,y 被指定成 "!"。x 仍被指定成 3,是因?yàn)槲覀兪菑耐鈱拥?lambda 取值的。這些行為讓我們回想到下列式子:

ghci> let x = 3; y = "!" in show x ++ y  
"3!"  

差別在于前述的值是 monadic,具有失敗可能性的 context。我們可以把其中任何一步代換成失敗的狀態(tài):

ghci> Nothing >>= (\x -> Just "!" >>= (\y -> Just (show x ++ y)))  
Nothing  
ghci> Just 3 >>= (\x -> Nothing >>= (\y -> Just (show x ++ y)))  
Nothing  
ghci> Just 3 >>= (\x -> Just "!" >>= (\y -> Nothing))  
Nothing  

第一行中,把 Nothing 喂給一個(gè)函數(shù),很自然地會(huì)回傳 Nothing。第二行里,我們把 Just 3 喂給一個(gè)函數(shù),所以 x 就成了 3。但我們把 Nothing 喂給內(nèi)層的 lambda 所有的結(jié)果就成了 Nothing,這也進(jìn)一步使得外層的 lambda 成了 Nothing。這就好比我們?cè)?nbsp;let expression 中來把值指定給變量一般。只差在我們這邊的值是 monadic value。

要再說得更清楚點(diǎn),我們來把 script 改寫成每行都處理一個(gè) Maybe:

foo :: Maybe String  
foo = Just 3   >>= (\x -> 
      Just "!" >>= (\y -> 
      Just (show x ++ y)))  

為了擺脫這些煩人的 lambda,Haskell 允許我們使用 do 表示法。他讓我們可以把先前的程序?qū)懗蛇@樣:

foo :: Maybe String  
foo = do  
    x <- Just 3  
    y <- Just "!"  
    Just (show x ++ y)  

這看起來好像讓我們不用在每一步都去檢查 Maybe 的值究竟是 Just 或 Nothing。這蠻方便的,如果在任何一個(gè)步驟我們?nèi)〕隽?nbsp;Nothing。那整個(gè) do 的結(jié)果就會(huì)是 Nothing。我們把整個(gè)責(zé)任都交給 >>=,他會(huì)幫我們處理所有 context 的問題。這邊的 do 表示法不過是另外一種語法的形式來串連所有的 monadic value 罷了。

在 do expression 中,每一行都是一個(gè) monadic value。要檢查處理的結(jié)果的話,就要使用 <-。如果我們拿到一個(gè) Maybe String,并用 <- 來綁定給一個(gè)變量,那個(gè)變量就會(huì)是一個(gè) String,就像是使用 >>= 來將 monadic value 帶給 lambda 一樣。至于 do expression 中的最后一個(gè)值,好比說 Just (show x ++ y),就不能用 <- 來綁定結(jié)果,因?yàn)槟菢拥膶懛ó?dāng)轉(zhuǎn)換成 >>= 的結(jié)果時(shí)并不合理。他必須要是所有 monadic value 黏起來后的總結(jié)果,要考慮到前面所有可能失敗的情形。

舉例來說,來看看下面這行:

ghci> Just 9 >>= (\x -> Just (x > 8))  
Just True  

由于 >>= 左邊的參數(shù)是一個(gè) Just 型態(tài)的值,當(dāng) lambda 被套用至 9 就會(huì)得到 Just True。如果我們重寫整個(gè)式子,改用 do 表示法:我們會(huì)得到:

marySue :: Maybe Bool  
marySue = do   
    x <- Just 9  
    Just (x > 8)  

如果我們比較這兩種寫法,就很容易看出為什么整個(gè) monadic value 的結(jié)果會(huì)是在 do 表示法中最后一個(gè) monadic value 的值。他串連了全面所有的結(jié)果。

我們走鋼索的仿真程序也可以改用 do 表示法重寫。landLeft 跟 landRight 接受一個(gè)鳥的數(shù)字跟一個(gè)竿子來產(chǎn)生一個(gè)包在 Just 中新的竿子。而在失敗的情況會(huì)產(chǎn)生 Nothing。我們使用 >>= 來串連所有的步驟,每一步都倚賴前一步的結(jié)果,而且都帶有可能失敗的 context。這邊有一個(gè)范例,先是有兩只鳥停在左邊,接著有兩只鳥停在右邊,然后是一只鳥停在左邊:

routine :: Maybe Pole  
routine = do  
    start <- return (0,0)  
    first <- landLeft 2 start  
    second <- landRight 2 first  
    landLeft 1 second  

我們來看看成功的結(jié)果:

ghci> routine  
Just (3,2) 

當(dāng)我們要把這些 routine 用具體寫出的 >>=,我們會(huì)這樣寫:return (0,0) >>= landLeft 2,而有了 do 表示法,每一行都必須是一個(gè) monadic value。所以我們清楚地把前一個(gè) Pole 傳給 landLeft 跟 landRight。如果我們查看我們綁定 Maybe 的變量,start 就是 (0,0),而 first 就會(huì)是 (2,0)。

由于 do 表示法是一行一行寫,他們會(huì)看起來很像是命令式的寫法。但實(shí)際上他們只是代表串行而已,每一步的值都倚賴前一步的結(jié)果,并帶著他們的 context 繼續(xù)下去。

我們?cè)僦匦聛砜纯慈绻覀儧]有善用 Maybe 的 monad 性質(zhì)的程序:

routine :: Maybe Pole  
    routine =   
        case Just (0,0) of   
            Nothing -> Nothing  
            Just start -> case landLeft 2 start of  
                Nothing -> Nothing  
                Just first -> case landRight 2 first of  
                    Nothing -> Nothing  
                    Just second -> landLeft 1 second  

在成功的情形下,Just (0,0) 變成了 start, 而 landLeft 2 start 的結(jié)果成了 first。

如果我們想在 do 表示法里面對(duì)皮爾斯丟出香蕉皮,我們可以這樣做:

routine :: Maybe Pole  
routine = do  
    start <- return (0,0)  
    first <- landLeft 2 start  
    Nothing  
    second <- landRight 2 first  
    landLeft 1 second  

當(dāng)我們?cè)?nbsp;do 表示法寫了一行運(yùn)算,但沒有用到 <- 來綁定值的話,其實(shí)實(shí)際上就是用了 >>,他會(huì)忽略掉計(jì)算的結(jié)果。我們只是要讓他們有序,而不是要他們的結(jié)果,而且他比寫成 _ <- Nothing 要來得漂亮的多。

你會(huì)問究竟我們何時(shí)要使用 do 表示法或是 >>=,這完全取決于你的習(xí)慣。在這個(gè)例子由于有每一步都倚賴于前一步結(jié)果的特性,所以我們使用 >>=。如果用 do 表示法,我們就必須清楚寫出鳥究竟是停在哪根竿子上,但其實(shí)每一次都是前一次的結(jié)果。不過他還是讓我們了解到怎么使用 do。

在 do 表示法中,我們其實(shí)可以用模式匹配來綁定 monadic value,就好像我們?cè)?nbsp;let 表達(dá)式,跟函數(shù)參數(shù)中使用模式匹配一樣。這邊來看一個(gè)在 do 表示法中使用模式匹配的范例:

justH :: Maybe Char  
justH = do  
    (x:xs) <- Just "hello"  
    return x 

我們用模式匹配來取得 "hello" 的第一個(gè)字符,然后回傳結(jié)果。所以 justH 計(jì)算會(huì)得到 Just 'h'。

如果模式匹配失敗怎么辦?當(dāng)定義一個(gè)函數(shù)的時(shí)候,一個(gè)模式不匹配就會(huì)跳到下一個(gè)模式。如果所有都不匹配,那就會(huì)造成錯(cuò)誤,整個(gè)程序就當(dāng)?shù)簟A硪环矫?,如果?nbsp;let 中進(jìn)行模式匹配失敗會(huì)直接造成錯(cuò)誤。畢竟在 let 表達(dá)式的情況下并沒有失敗就跳下一個(gè)的設(shè)計(jì)。至于在 do 表示法中模式匹配失敗的話,那就會(huì)調(diào)用 fail 函數(shù)。他定義在 Monad 的 type class 定義豬。他允許在現(xiàn)在的 monad context 底下,失敗只會(huì)造成失敗而不會(huì)讓整個(gè)程序當(dāng)?shù)?。他缺省的?shí)作如下:

fail :: (Monad m) => String -> m a  
fail msg = error msg  

可見缺省的實(shí)作的確是讓程序掛掉,但在某些考慮到失敗的可能性的 Monad(像是 Maybe)常常會(huì)有他們自己的實(shí)作。對(duì)于 Maybe,他的實(shí)作像是這樣:

fail _ = Nothing

他忽略錯(cuò)誤消息,并直接回傳 Nothing。所以當(dāng)在 do 表示法中的 Maybe 模式匹配失敗的時(shí)候,整個(gè)結(jié)果就會(huì)是 Nothing。這種方式比起讓程序掛掉要好多了。這邊來看一下 Maybe 模式匹配失敗的范例:

wopwop :: Maybe Char  
wopwop = do  
    (x:xs) <- Just ""  
    return x  

模式匹配的失敗,所以那一行的效果相當(dāng)于一個(gè) Nothing。我們來看看執(zhí)行結(jié)果:

ghci> wopwop  
Nothing  

這樣模式匹配的失敗只會(huì)限制在我們 monad 的 context 中,而不是整個(gè)程序的失敗。這種處理方式要好多了。

List Monad

我們已經(jīng)了解了 Maybe 可以被看作具有失敗可能性 context 的值,也見識(shí)到如何用 >>= 來把這些具有失敗考量的值傳給函數(shù)。在這一個(gè)章節(jié)中,我們要看一下如何利用 list 的 monadic 的性質(zhì)來寫 non-deterministic 的程序。

我們已經(jīng)討論過在把 list 當(dāng)作 applicatives 的時(shí)候他們具有 non-deterministic 的性質(zhì)。像 5 這樣一個(gè)值是 deterministic 的。他只有一種結(jié)果,而且我們清楚的知道他是什么結(jié)果。另一方面,像 [3,8,9] 這樣的值包含好幾種結(jié)果,所以我們能把他看作是同時(shí)具有好幾種結(jié)果的值。把 list 當(dāng)作 applicative functors 展示了這種特性:

ghci> (*) <$> [1,2,3] <*> [10,100,1000]  
[10,100,1000,20,200,2000,30,300,3000]  

將左邊 list 中的元素乘上右邊 list 中的元素這樣所有的組合全都被放進(jìn)結(jié)果的 list 中。當(dāng)處理 non-determinism 的時(shí)候,這代表我們有好幾種選擇可以選,我們也會(huì)每種選擇都試試看,因此最終的結(jié)果也會(huì)是一個(gè) non-deterministic 的值。只是包含更多不同可能罷了。

non-determinism 這樣的 context 可以被漂亮地用 monad 來考慮。所以我們這就來看看 list 的 Monad instance 的定義:

instance Monad [] where  
    return x = [x]  
    xs >>= f = concat (map f xs)  
    fail _ = []  

return 跟 pure 是做同樣的事,所以我們應(yīng)該算已經(jīng)理解了 return 的部份。他接受一個(gè)值,并把他放進(jìn)一個(gè)最小的一個(gè) context 中。換種說法,就是他做了一個(gè)只包含一個(gè)元素的 list。這樣對(duì)于我們想要操作普通值的時(shí)候很有用,可以直接把他包起來變成 non-deterministic value。

要理解 >>= 在 list monad 的情形下是怎么運(yùn)作的,讓我們先來回歸基本。>>= 基本上就是接受一個(gè)有 context 的值,把他喂進(jìn)一個(gè)只接受普通值的函數(shù),并回傳一個(gè)具有 context 的值。如果操作的函數(shù)只會(huì)回傳普通值而不是具有 context 的值,那 >>= 在操作一次后就會(huì)失效,因?yàn)?context 不見了。讓我們來試著把一個(gè) non-deterministic value 塞到一個(gè)函數(shù)中:

ghci> [3,4,5] >>= \x -> [x,-x]  
[3,-3,4,-4,5,-5]  

當(dāng)我們對(duì) Maybe 使用 >>=,是有考慮到可能失敗的 context。在這邊 >>= 則是有考慮到 non-determinism。[3,4,5] 是一個(gè) non-deterministic value,我們把他喂給一個(gè)回傳 non-deterministic value 的函數(shù)。那結(jié)果也會(huì)是 non-deterministic。而且他包含了所有從 [3,4,5] 取值,套用 \x -> [x,-x] 后的結(jié)果。這個(gè)函數(shù)他接受一個(gè)數(shù)值并產(chǎn)生兩個(gè)數(shù)值,一個(gè)原來的數(shù)值與取過負(fù)號(hào)的數(shù)值。當(dāng)我們用 >>= 來把一個(gè) list 喂給這個(gè)函數(shù),所有在 list 中的數(shù)值都保留了原有的跟取負(fù)號(hào)過的版本。x 會(huì)針對(duì) list 中的每個(gè)元素走過一遍。

要看看結(jié)果是如何算出來的,只要看看實(shí)作就好了。首先我們從 [3,4,5] 開始。然后我們用 lambda 映射過所有元素得到:

[[3,-3],[4,-4],[5,-5]]      

lambda 會(huì)掃過每個(gè)元素,所以我們有一串包含一堆 list 的 list,最后我們?cè)诎堰@些 list 壓扁,得到一層的 list。這就是我們得到 non-deterministic value 的過程。

non-determinism 也有考慮到失敗的可能性。[] 其實(shí)等價(jià)于 Nothing,因?yàn)樗裁唇Y(jié)果也沒有。所以失敗等同于回傳一個(gè)空的 list。所有的錯(cuò)誤消息都不用。讓我們來看看范例:

ghci> [] >>= \x -> ["bad","mad","rad"]  
[]  
ghci> [1,2,3] >>= \x -> []  
[] 

第一行里面,一個(gè)空的 list 被丟給 lambda。因?yàn)?list 沒有任何元素,所以函數(shù)收不到任何東西而產(chǎn)生空的 list。這跟把 Nothing 喂給函數(shù)一樣。第二行中,每一個(gè)元素都被喂給函數(shù),但所有元素都被丟掉,而只回傳一個(gè)空的 list。因?yàn)樗械脑囟荚斐闪耸。哉麄€(gè)結(jié)果也代表失敗。

就像 Maybe 一樣,我們可以用 >>= 把他們串起來:

ghci> [1,2] >>= \n -> ['a','b'] >>= \ch -> return (n,ch)  
[(1,'a'),(1,'b'),(2,'a'),(2,'b')]  

[1,2] 被綁定到 n 而 ['a','b'] 被綁定到 ch。最后我們用 return (n,ch) 來把他放到一個(gè)最小的 context 中。在這個(gè)案例中,就是把 (n,ch) 放到 list 中,這代表最低程度的 non-determinism。整套結(jié)構(gòu)要表達(dá)的意思就是對(duì)于 [1,2] 的每個(gè)元素,以及 ['a','b'] 的每個(gè)元素,我們產(chǎn)生一個(gè) tuple,每項(xiàng)分別取自不同的 list。

一般來說,由于 return 接受一個(gè)值并放到最小的 context 中,他不會(huì)多做什么額外的東西僅僅是展示出結(jié)果而已。

當(dāng)你要處理 non-deterministic value 的時(shí)候,你可以把 list 中的每個(gè)元素想做計(jì)算路線的一個(gè) branch。

這邊把先前的表達(dá)式用 do 重寫:

listOfTuples :: [(Int,Char)]  
listOfTuples = do  
    n <- [1,2]  
    ch <- ['a','b']  
    return (n,ch)  

這樣寫可以更清楚看到 n 走過 [1,2] 中的每一個(gè)值,而 ch 則取過 ['a','b'] 中的每個(gè)值。正如 Maybe 一般,我們從 monadic value 中取出普通值然后喂給函數(shù)。>>= 會(huì)幫我們處理好一切 context 相關(guān)的問題,只差在這邊的 context 指的是 non-determinism。

使用 do 來對(duì) list 操作讓我們回想起之前看過的一些東西。來看看下列的片段:

ghci> [ (n,ch) | n <- [1,2], ch <- ['a','b'] ]  
[(1,'a'),(1,'b'),(2,'a'),(2,'b')]  

沒錯(cuò),就是 list comprehension。在先前的范例中,n 會(huì)走過 [1,2] 的每個(gè)元素,而 ch 會(huì)走過 ['a','b'] 的每個(gè)元素。同時(shí)我們又把 (n,ch) 放進(jìn)一個(gè) context 中。這跟 list comprehension 的目的一樣,只是我們?cè)?list comprehension 里面不用在最后寫一個(gè) return 來得到 (n,ch) 的結(jié)果。

實(shí)際上,list comprehension 不過是一個(gè)語法糖。不論是 list comprehension 或是用 do 表示法來表示,他都會(huì)轉(zhuǎn)換成用 >>= 來做計(jì)算。

List comprehension 允許我們 filter 我們的結(jié)果。舉例來說,我們可以只要包含 7 在表示位數(shù)里面的數(shù)值。

ghci> [ x | x <- [1..50], '7' `elem` show x ]  
[7,17,27,37,47]  

我們用 show 跟 x 來把數(shù)值轉(zhuǎn)成字串,然后檢查 '7' 是否包含在字串里面。要看看 filtering 要如何轉(zhuǎn)換成用 list monad 來表達(dá),我們可以考慮使用 guard 函數(shù),還有 MonadPlus 這個(gè) type class。MonadPlus 這個(gè) type class 是用來針對(duì)可以同時(shí)表現(xiàn)成 monoid 的 monad。下面是他的定義:

class Monad m => MonadPlus m where  
    mzero :: m a  
    mplus :: m a -> m a -> m a  

mzero 是其實(shí)是 Monoid 中 mempty 的同義詞,而 mplus 則對(duì)應(yīng)到 mappend。因?yàn)?list 同時(shí)是 monoid 跟 monad,他們可以是 MonadPlus 的 instance。

instance MonadPlus [] where  
    mzero = []  
    mplus = (++) 

對(duì)于 list 而言,mzero 代表的是不產(chǎn)生任何結(jié)果的 non-deterministic value,也就是失敗的結(jié)果。而 mplus 則把兩個(gè) non-deterministic value 結(jié)合成一個(gè)。guard 這個(gè)函數(shù)被定義成下列形式:

guard :: (MonadPlus m) => Bool -> m ()  
guard True = return ()  
guard False = mzero  

這函數(shù)接受一個(gè)布林值,如果他是 True 就回傳一個(gè)包在缺省 context 中的 ()。如果他失敗就產(chǎn)生 mzero。

ghci> guard (5 > 2) :: Maybe ()  
Just ()  
ghci> guard (1 > 2) :: Maybe ()  
Nothing  
ghci> guard (5 > 2) :: [()]  
[()]  
ghci> guard (1 > 2) :: [()]  
[]  

看起來蠻有趣的,但用起來如何呢?我們可以用他來過濾 non-deterministic 的計(jì)算。

ghci> [1..50] >>= (\x -> guard ('7' `elem` show x) >> return x)  
[7,17,27,37,47]  

這邊的結(jié)果跟我們之前 list comprehension 的結(jié)果一致。究竟 guard 是如何辦到的?我們先看看 guard 跟 >> 是如何交互:

ghci> guard (5 > 2) >> return "cool" :: [String]  
["cool"]  
ghci> guard (1 > 2) >> return "cool" :: [String]  
[]  

如果 guard 成功的話,結(jié)果就會(huì)是一個(gè)空的 tuple。接著我們用 >> 來忽略掉空的 tuple,而呈現(xiàn)不同的結(jié)果。另一方面,如果 guard 失敗的話,后面的 return 也會(huì)失敗。這是因?yàn)橛?nbsp;>>= 把空的 list 喂給函數(shù)總是會(huì)回傳空的 list?;旧?nbsp;guard 的意思就是:如果一個(gè)布林值是 False 那就產(chǎn)生一個(gè)失敗狀態(tài),不然的話就回傳一個(gè)基本的 ()。這樣計(jì)算就可以繼續(xù)進(jìn)行。

這邊我們把先前的范例用 do 改寫:

sevensOnly :: [Int]  
sevensOnly = do  
    x <- [1..50]  
    guard ('7' `elem` show x)  
    return x  

如果我們不寫最后一行 return x,那整個(gè) list 就會(huì)是包含一堆空 tuple 的 list。

把上述范例寫成 list comprehension 的話就會(huì)像這樣:

ghci> [ x | x <- [1..50], '7' `elem` show x ]  
[7,17,27,37,47]  

所以 list comprehension 的 filtering 基本上跟 guard 是一致的。

A knight's quest

這邊來看一個(gè)可以用 non-determinism 解決的問題。假設(shè)你有一個(gè)西洋棋盤跟一只西洋棋中的騎士擺在上面。我們希望知道是否這只騎士可以在三步之內(nèi)移到我們想要的位置。我們只要用一對(duì)數(shù)值來表示騎士在棋盤上的位置。第一個(gè)數(shù)值代表棋盤的行,而第二個(gè)數(shù)值代表棋盤的列。

我們先幫騎士的位置定義一個(gè) type synonym。

type KnightPos = (Int,Int)      

假設(shè)騎士現(xiàn)在是在 (6,2)。究竟他能不能夠在三步內(nèi)移動(dòng)到 (6,1) 呢?你可能會(huì)先考慮究竟哪一步是最佳的一步。但不如全部一起考慮吧!要好好利用所謂的 non-determinism。所以我們不是只選擇一步,而是選擇全部。我們先寫一個(gè)函數(shù)回傳所有可能的下一步:

moveKnight :: KnightPos -> [KnightPos]  
moveKnight (c,r) = do  
    (c',r') <- [(c+2,r-1),(c+2,r+1),(c-2,r-1),(c-2,r+1)  
                ,(c+1,r-2),(c+1,r+2),(c-1,r-2),(c-1,r+2)  
                ]  
    guard (c' `elem` [1..8] && r' `elem` [1..8])
    return (c',r')  

騎士有可能水平或垂直移動(dòng)一步或二步,但問題是他們必須要同時(shí)水平跟垂直移動(dòng)。(c',r') 走過 list 中的每一個(gè)元素,而 guard 會(huì)保證產(chǎn)生的結(jié)果會(huì)停留在棋盤上。如果沒有,那就會(huì)產(chǎn)生一個(gè)空的 list,表示失敗的結(jié)果,return (c',r') 也就不會(huì)被執(zhí)行。

這個(gè)函數(shù)也可以不用 list monad 來寫,但我們這邊只是寫好玩的。下面是一個(gè)用 filter 實(shí)現(xiàn)的版本:

moveKnight :: KnightPos -> [KnightPos]  
moveKnight (c,r) = filter onBoard  
    [(c+2,r-1),(c+2,r+1),(c-2,r-1),(c-2,r+1)  
    ,(c+1,r-2),(c+1,r+2),(c-1,r-2),(c-1,r+2)  
    ]  
    where onBoard (c,r) = c `elem` [1..8] && r `elem` [1..8]

兩個(gè)函數(shù)做的都是相同的事,所以選個(gè)你喜歡的吧。

ghci> moveKnight (6,2)  
[(8,1),(8,3),(4,1),(4,3),(7,4),(5,4)]  
ghci> moveKnight (8,1)  
[(6,2),(7,3)] 

我們接受一個(gè)位置然后產(chǎn)生所有可能的移動(dòng)方式。所以我們有一個(gè) non-deterministic 的下一個(gè)位置。我們用 >>= 來喂給 moveKnight。接下來我們就可以寫一個(gè)三步內(nèi)可以達(dá)到的所有位置:

in3 :: KnightPos -> [KnightPos]  
in3 start = do   
    first <- moveKnight start  
    second <- moveKnight first  
    moveKnight second  

如果你傳 (6,2),得到的 list 會(huì)很大,因?yàn)闀?huì)有不同種方式來走到同樣的一個(gè)位置。我們也可以不用 do 來寫:

in3 start = return start >>= moveKnight >>= moveKnight >>= moveKnight      

第一次 >>= 給我們移動(dòng)一步的所有結(jié)果,第二次 >>= 給我們移動(dòng)兩步的所有結(jié)果,第三次則給我們移動(dòng)三步的所有結(jié)果。

用 return 來把一個(gè)值放進(jìn)缺省的 context 然后用 >>= 喂給一個(gè)函數(shù)其實(shí)跟函數(shù)調(diào)用是同樣的,只是用不同的寫法而已。 接著我們寫一個(gè)函數(shù)接受兩個(gè)位置,然后可以測(cè)試是否可以在三步內(nèi)從一個(gè)位置移到另一個(gè)位置:

canReachIn3 :: KnightPos -> KnightPos -> Bool  
canReachIn3 start end = end `elem` in3 start  

我們產(chǎn)生所有三步的可能位置,然后看看其中一個(gè)位置是否在里面。所以我們可以看看是否可以在三步內(nèi)從 (6,2) 走到 (6,1):

ghci> (6,2) `canReachIn3` (6,1)  
True 

那從 (6,2) 到 (7,3) 呢?

ghci> (6,2) `canReachIn3` (7,3)  
False  

答案是不行。你可以修改函數(shù)改成當(dāng)可以走到的時(shí)候,他還會(huì)告訴你實(shí)際的步驟。之后你也可以改成不只限定成三步,可以任意步。

Monad laws (單子律)

正如 applicative functors 以及 functors,Monad 也有一些要遵守的定律。我們定義一個(gè) Monad 的 instance 并不代表他是一個(gè) monad,只代表他被定義成那個(gè) type class 的 instance。一個(gè)型態(tài)要是 monad,則必須遵守單子律。這些定律讓我們可以對(duì)這個(gè)型態(tài)的行為做一些合理的假設(shè)。

Haskell 允許任何型態(tài)是任何 type class 的 instance。但他不會(huì)檢查單子律是否有被遵守,所以如果我們要寫一個(gè) Monad 的 instance,那最好我們確定他有遵守單子律。我們可以不用擔(dān)心標(biāo)準(zhǔn)函式庫中的型態(tài)是否有遵守單子律。但之后我們定義自己的型態(tài)時(shí),我們必須自己檢查是否有遵守單子律。不用擔(dān)心,他們不會(huì)很復(fù)雜。

Left identity

單子律的第一項(xiàng)說當(dāng)我們接受一個(gè)值,將他用 return 放進(jìn)一個(gè)缺省的 context 并把他用 >>= 喂進(jìn)一個(gè)函數(shù)的結(jié)果,應(yīng)該要跟我們直接做函數(shù)調(diào)用的結(jié)果一樣。

  • retrun x >>= f 應(yīng)該等于 f x

如果你是把 monadic value 視為把一個(gè)值放進(jìn)最小的 context 中,僅僅是把同樣的值放進(jìn)結(jié)果中的話, 那這個(gè)定律應(yīng)該很直覺。因?yàn)榘堰@個(gè)值放進(jìn) context 中然后丟給函數(shù),應(yīng)該要跟直接把這個(gè)值丟給函數(shù)做調(diào)用應(yīng)該沒有差別。

對(duì)于 Maybe monad,return 被定義成 Just。Maybe monad 講的是失敗的可能性,如果我們有普通值要把他放進(jìn) context 中,那把這個(gè)動(dòng)作當(dāng)作是計(jì)算成功應(yīng)該是很合理的,畢竟我們都知道那個(gè)值是很具體的。這邊有些范例:

ghci> return 3 >>= (\x -> Just (x+100000))  
Just 100003  
ghci> (\x -> Just (x+100000)) 3  
Just 100003  

對(duì)于 list monad 而言,return 是把值放進(jìn)一個(gè) list 中,變成只有一個(gè)元素的 list。>>= 則會(huì)走過 list 中的每個(gè)元素,并把他們丟給函數(shù)做運(yùn)算,但因?yàn)樵趩我辉氐?list 中只有一個(gè)值,所以跟直接對(duì)那元素做運(yùn)算是等價(jià)的:

ghci> return "WoM" >>= (\x -> [x,x,x])  
["WoM","WoM","WoM"]  
ghci> (\x -> [x,x,x]) "WoM"  
["WoM","WoM","WoM"]  

至于 IO,我們已經(jīng)知道 return 并不會(huì)造成副作用,只不過是在結(jié)果中呈現(xiàn)原有值。所以這個(gè)定律對(duì)于 IO 也是有效的。

Right identity

單子律的第二個(gè)規(guī)則是如果我們有一個(gè) monadic value,而且我們把他用 >>= 喂給 return,那結(jié)果就會(huì)是原有的 monadic value。

  • m >>= return 會(huì)等于 m

這一個(gè)可能不像第一定律那么明顯,但我們還是來看看為什么會(huì)遵守這條。當(dāng)我們把一個(gè) monadic value 用 >>= 喂給函數(shù),那些函數(shù)是接受普通值并回傳具有 context 的值。return 也是在他們其中。如果你仔細(xì)看他的型態(tài),return 是把一個(gè)普通值放進(jìn)一個(gè)最小 context 中。這就表示,對(duì)于 Maybe 他并沒有造成任何失敗的狀態(tài),而對(duì)于 list 他也沒有多加 non-determinism。

ghci> Just "move on up" >>= (\x -> return x)  
Just "move on up"  
ghci> [1,2,3,4] >>= (\x -> return x)  
[1,2,3,4]  
ghci> putStrLn "Wah!" >>= (\x -> return x)  
Wah!  

如果我們仔細(xì)查看 list monad 的范例,會(huì)發(fā)現(xiàn) >>= 的實(shí)作是:

xs >>= f = concat (map f xs)      

所以當(dāng)我們將 [1,2,3,4] 丟給 return,第一個(gè) return 會(huì)把 [1,2,3,4] 映射成 [[1],[2],[3],[4]],然后再把這些小 list 串接成我們?cè)械?list。

Left identity 跟 right identity 是描述 return 的行為。他重要的原因是因?yàn)樗哑胀ㄖ缔D(zhuǎn)換成具有 context 的值,如果他出錯(cuò)的話會(huì)很頭大。

Associativity

單子律最后一條是說當(dāng)我們用 >>= 把一串 monadic function 串在一起,他們的先后順序不應(yīng)該影響結(jié)果:

  • (m >>= f) >>= g 跟 m >>= (\x -> f x >>= g) 是相等的

究竟這邊說的是什么呢?我們有一個(gè) monadic value m,以及兩個(gè) monadic function f 跟 g。當(dāng)我們寫下 (m >>= f) >>= g,代表的是我們把 m 喂給 f,他的結(jié)果是一個(gè) monadic value。然后我們把這個(gè)結(jié)果喂給 g。而在 m >>= (\x -> f x >>= g) 中,我們接受一個(gè) monadic value 然后喂給一個(gè)函數(shù),這個(gè)函數(shù)會(huì)把 f x 的結(jié)果丟給 g。我們不太容易直接看出兩者相同,所以先來看個(gè)范例比較好理解。

還記得之前皮爾斯的范例嗎?要仿真鳥停在他的平衡竿上,我們把好幾個(gè)函數(shù)串在一起

ghci> return (0,0) >>= landRight 2 >>= landLeft 2 >>= landRight 2  
Just (2,4) 

從 Just (0,0) 出發(fā),然后把值傳給 landRight 2。他的結(jié)果又被綁到下一個(gè) monadic function,以此類推。如果我們用括號(hào)清楚標(biāo)出優(yōu)先級(jí)的話會(huì)是這樣:

ghci> ((return (0,0) >>= landRight 2) >>= landLeft 2) >>= landRight 2  
Just (2,4)  

我們也可以改寫成這樣:

return (0,0) >>= (\x -> 
landRight 2 x >>= (\y -> 
landLeft 2 y >>= (\z -> 
landRight 2 z)))     

return (0,0) 等價(jià)于 Just (0,0),當(dāng)我們把他喂給 lambda,里面的 x 就等于 (0,0)。landRight 接受一個(gè)數(shù)值跟 pole,算出來的結(jié)果是 Just (0,2) 然后把他喂給另一個(gè) lambda,里面的 y 就變成了 (0,2)。這樣的操作持續(xù)下去,直到最后一只鳥降落,而得到 Just (2,4) 的結(jié)果,這也是整個(gè)操作的總結(jié)果。

這些 monadic function 的優(yōu)先級(jí)并不重要,重點(diǎn)是他們的意義。從另一個(gè)角度來看這個(gè)定律:考慮兩個(gè)函數(shù) f 跟 g,將兩個(gè)函數(shù)組合起來的定義像是這樣:

(.) :: (b -> c) -> (a -> b) -> (a -> c)  
f . g = (\x -> f (g x))  

如果 g 的型態(tài)是 a -> b 且 f 的型態(tài)是 b -> c,我們可以把他們合成一個(gè)型態(tài)是 a -> c 的新函數(shù)。所以中間的參數(shù)都有自動(dòng)帶過。現(xiàn)在假設(shè)這兩個(gè)函數(shù)是 monadic function,也就是說如果他們的回傳值是 monadic function?如果我們有一個(gè)函數(shù)他的型態(tài)是 a -> m b,我們并不能直接把結(jié)果丟給另一個(gè)型態(tài)為 b -> m c 的函數(shù),因?yàn)楹笳咧唤邮苄蛻B(tài)為 b 的普通值。然而,我們可以用 >>= 來做到我們想要的事。有了 >>=,我們可以合成兩個(gè) monadic function:

(<=<) :: (Monad m) => (b -> m c) -> (a -> m b) -> (a -> m c)  
f <=< g = (\x -> g x >>= f)  

所以現(xiàn)在我們可以合成兩個(gè) monadic functions:

ghci> let f x = [x,-x]  
ghci> let g x = [x*3,x*2]  
ghci> let h = f <=< g  
ghci> h 3  
[9,-9,6,-6]  

至于這跟結(jié)合律有什么關(guān)系呢?當(dāng)我們把這定律看作是合成的定律,他就只是說了 f <=< (g <=< h) 跟 (f <=< g) <=< h 應(yīng)該等價(jià)。只是他是針對(duì) monad 而已。

如果我們把頭兩個(gè)單子律用 <=< 改寫,那 left identity 不過就是說對(duì)于每個(gè) monadic function f,f <=< return 跟 f 是等價(jià),而 right identity 說 return <=< f 跟 f 是等價(jià)。

如果看看普通函數(shù)的情形,就會(huì)發(fā)現(xiàn)很像,(f . g) . h 等價(jià)于 f . (g . h),f . id 跟 f 等價(jià),且 id . f 等價(jià)于 f。

在這一章中,我們查看了 monad 的基本性質(zhì),而且也了解了 Maybe monad 跟 list monad 的運(yùn)作方式。在下一章,我們會(huì)看看其他一些有特色的 monad,我們也會(huì)學(xué)到如何定義自己的 monad。



以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)