在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ C/ 來看看幾種 Monad
函數(shù)的語法
Resource
Zippers 資料結(jié)構(gòu)
函數(shù)式地思考來解決問題
簡介
遞回
輸入與輸出
FAQ
Types and Typeclasses
再來看看更多 Monad
來看看幾種 Monad
高階函數(shù)
構(gòu)造我們自己的 Types 和 Typeclasses
從零開始
Functors, Applicative Functors 與 Monoids
模組 (Modules)

來看看幾種 Monad

當(dāng)我們第一次談到 Functor 的時(shí)候,我們了解到他是一個抽象概念,代表是一種可以被 map over 的值。然后我們再將其概念提升到 Applicative Functor,他代表一種帶有 context 的型態(tài),我們可以用函數(shù)操作他而且同時(shí)還保有他的 context。

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

http://wiki.jikexueyuan.com/project/haskell-guide/images/smugpig.png" alt="" />

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

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

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

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

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

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

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

來看看 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 代表可能會失敗的 computation,[a] 代表同時(shí)有好多結(jié)果的 computation (non-deterministic computation),而 IO a 代表會有 side-effects 的 computation。

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

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

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

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

動手做做看: Maybe Monad

http://wiki.jikexueyuan.com/project/haskell-guide/images/buddha.png" alt="" />

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

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

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

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

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

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

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

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é)果一定會是 Nothing。

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

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

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

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

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

我們呼叫 applyMaybe 而不呼叫 >>=。他接受 Maybe a 跟一個回傳 Maybe b 的函數(shù),并套用函數(shù)至 Maybe a。

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

我們套用一個 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)我們試圖套用到 Nothing,那整個結(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,那整個結(jié)果就是 Nothing。如果右邊的函數(shù)是 Nothing,那結(jié)果也會是 Nothing。這跟我們之前把 Maybe 當(dāng)作 applicative 時(shí),過程中有任何一個 Nothing 整個結(jié)果就會是 Nothing 一樣。

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

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

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

Monad type class

正如 functors 有 Functor 這個 type class,而 applicative functors 有一個 Applicative 這個 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  

http://wiki.jikexueyuan.com/project/haskell-guide/images/kid.png" alt="" />

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

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

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

http://wiki.jikexueyuan.com/project/haskell-guide/images/tur2.png" alt="" />

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

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

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

我們知道了 Monad typeclass 長什么樣子,我們來看一下 MaybeMonad instance。

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

returnpure是等價(jià)的。這沒什么困難的。我們跟我們在定義Applicative的時(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 而我們又對 Maybe 操作過 pure 了。至于下兩行就比較有趣點(diǎn)。

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

走鋼索

http://wiki.jikexueyuan.com/project/haskell-guide/images/pierre.png" alt="" />

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

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

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

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

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

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

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

接下來,我們定義一個函數(shù)他接受一個數(shù)字,然后把他放在竿子的左邊,還有另外一個函數(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)  

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

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

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

x -: f = f x

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

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

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

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

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

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

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

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

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

表面看起來沒什么問題,但如果你仔細(xì)看的話,有一瞬間是右邊有四只,但左邊沒有鳥。要修正這個錯誤,我們要重新檢視 landLeftlandRight。我們其實(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)在會檢查是否有太多鳥會造成皮爾斯失去平衡。我們用 guards 來檢查是否有差異超過三的情況。如果沒有,那就包一個在 Just 中的新的 pole,如果是,那就回傳 Nothing

再來執(zhí)行看看:

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

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

我們需要一種方法可以把拿到的 Maybe Pole 塞到拿 Pole 的函數(shù)中,然后回傳 Maybe Pole。而我們有 >>=,他對 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,所以我們用 >>= 來接受一個有 context 的值然后拿給 landLeft 2。>>= 的確讓我們把 Maybe 當(dāng)作有 context 的值,因?yàn)楫?dāng)我們丟 NothinglandLeft 2 的時(shí)候,結(jié)果會是 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 回傳一個 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)  

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

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

http://wiki.jikexueyuan.com/project/haskell-guide/images/banana.png" alt="" />

正如預(yù)期的,最后的情形代表了失敗的情況。我們再進(jìn)一步看看這是怎么產(chǎn)生的。首先 return(0,0) 放到一個最小的 context 中,得到 Just (0,0)。然后是 Just (0.0) >>= landLeft 1。由于 Just (0,0) 是一個 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,也就自動得到了 Nothing。

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

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

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

現(xiàn)在我們能把香蕉皮串到我們的過程中。他絕對會讓遇到的人滑倒。他完全不管前面的狀態(tài)是什么都會產(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  

一般來講,碰到一個完全忽略前面狀態(tài)的函數(shù),他就應(yīng)該只會回傳他想回傳的值而已。但碰到 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 的值的寫法。他會長得像這樣:

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  

http://wiki.jikexueyuan.com/project/haskell-guide/images/centaur.png" alt="" />

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

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

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

do 表示法

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

來看一下熟悉的例子:

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

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

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

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

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

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

為了擺脫這些煩人的 lambda,Haskell 允許我們使用 do 表示法。他讓我們可以把先前的程式寫成這樣:

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

http://wiki.jikexueyuan.com/project/haskell-guide/images/owld.png" alt="" />

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

do expression 中,每一行都是一個 monadic value。要檢查處理的結(jié)果的話,就要使用 <-。如果我們拿到一個 Maybe String,并用 <- 來綁定給一個變數(shù),那個變數(shù)就會是一個 String,就像是使用 >>= 來將 monadic value 帶給 lambda 一樣。至于 do expression 中的最后一個值,好比說 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ù)是一個 Just 型態(tài)的值,當(dāng) lambda 被套用至 9 就會得到 Just True。如果我們重寫整個式子,改用 do 表示法:我們會得到:

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

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

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

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 用具體寫出的 >>=,我們會這樣寫:return (0,0) >>= landLeft 2,而有了 do 表示法,每一行都必須是一個 monadic value。所以我們清楚地把前一個 Pole 傳給 landLeftlandRight。如果我們檢視我們綁定 Maybe 的變數(shù),start 就是 (0,0),而 first 就會是 (2,0)。

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

我們再重新來看看如果我們沒有善用 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 表示法里面對皮爾斯丟出香蕉皮,我們可以這樣做:

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

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

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

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

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

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

如果模式匹配失敗怎么辦?當(dāng)定義一個函數(shù)的時(shí)候,一個模式不匹配就會跳到下一個模式。如果所有都不匹配,那就會造成錯誤,整個程式就當(dāng)?shù)?。另一方面,如果?let 中進(jìn)行模式匹配失敗會直接造成錯誤。畢竟在 let 表達(dá)式的情況下并沒有失敗就跳下一個的設(shè)計(jì)。至于在 do 表示法中模式匹配失敗的話,那就會呼叫 fail 函數(shù)。他定義在 Monad 的 type class 定義豬。他允許在現(xiàn)在的 monad context 底下,失敗只會造成失敗而不會讓整個程式當(dāng)?shù)簟KA(yù)設(shè)的實(shí)作如下:

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

可見預(yù)設(shè)的實(shí)作的確是讓程式掛掉,但在某些考慮到失敗的可能性的 Monad(像是 Maybe)常常會有他們自己的實(shí)作。對于 Maybe,他的實(shí)作像是這樣:

fail _ = Nothing

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

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

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

ghci> wopwop  
Nothing  

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

List Monad

http://wiki.jikexueyuan.com/project/haskell-guide/images/deadcat.png" alt="" />

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

我們已經(jīng)討論過在把 list 當(dāng)作 applicatives 的時(shí)候他們具有 non-deterministic 的性質(zhì)。像 5 這樣一個值是 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í)候,這代表我們有好幾種選擇可以選,我們也會每種選擇都試試看,因此最終的結(jié)果也會是一個 non-deterministic 的值。只是包含更多不同可能罷了。

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

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

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

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

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

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

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

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

lambda 會掃過每個元素,所以我們有一串包含一堆 list 的 list,最后我們在把這些 list 壓扁,得到一層的 list。這就是我們得到 non-deterministic value 的過程。

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

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

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

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

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

http://wiki.jikexueyuan.com/project/haskell-guide/images/concatmap.png" alt="" />

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

這函數(shù)接受一個布林值,如果他是 True 就回傳一個包在預(yù)設(shè) 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é)果就會是一個空的 tuple。接著我們用 >> 來忽略掉空的 tuple,而呈現(xiàn)不同的結(jié)果。另一方面,如果 guard 失敗的話,后面的 return 也會失敗。這是因?yàn)橛?>>= 把空的 list 喂給函數(shù)總是會回傳空的 list?;旧?guard