Haskell 的一些特色,像是純粹性,高端函數(shù),algebraic data types,typeclasses,這些讓我們可以從更高的角度來看到 polymorphism 這件事。不像 OOP 當(dāng)中需要從龐大的型態(tài)階層來思考。我們只需要看看手邊的型態(tài)的行為,將他們跟適當(dāng)?shù)?typeclass 對(duì)應(yīng)起來就可以了。像 Int 的行為跟很多東西很像。好比說他可以比較相不相等,可以從大到小排列,也可以將他們一一窮舉出來。
Typeclass 的運(yùn)用是很隨意的。我們可以定義自己的數(shù)據(jù)型態(tài),然后描述他可以怎樣被操作,跟 typeclass 關(guān)聯(lián)起來便定義了他的行為。由于 Haskell 強(qiáng)大的型態(tài)系統(tǒng),這讓我們只要讀函數(shù)的型態(tài)宣告就可以知道很多信息。typeclass 可以定義得很抽象很 general。我們之前有看過 typeclass 定義了可以比較兩個(gè)東西是否相等,或是定義了可以比較兩個(gè)東西的大小。這些是既抽象但又描述簡潔的行為,但我們不會(huì)認(rèn)為他們有什么特別之處,因?yàn)槲覀儠r(shí)常碰到他們。最近我們看過了 functor,基本上他們是一群可以被 map over 的對(duì)象。這是其中一個(gè)例子能夠抽象但又漂亮地描述行為。在這一章中,我們會(huì)詳加闡述 functors,并會(huì)提到比較強(qiáng)一些的版本,也就是 applicative functors。我們也會(huì)提到 monoids。
我們已經(jīng)在之前的章節(jié)提到 functors。如果你還沒讀那個(gè)章節(jié),也許你應(yīng)該先去看看?;蚴悄阒苯蛹傺b你已經(jīng)讀過了。
來快速復(fù)習(xí)一下:Functors 是可以被 map over 的對(duì)象,像是 lists,Maybe,trees 等等。在 Haskell 中我們是用 Functor 這個(gè) typeclass 來描述他。這個(gè) typeclass 只有一個(gè) method,叫做 fmap,他的型態(tài)是 fmap :: (a -> b) -> f a -> f b。這型態(tài)說明了如果給我一個(gè)從 a 映到 b 的函數(shù),以及一個(gè)裝了 a 的盒子,我會(huì)回給你一個(gè)裝了 b 的盒子。就好像用這個(gè)函數(shù)將每個(gè)元素都轉(zhuǎn)成 b 一樣
*給一點(diǎn)建議*。這盒子的比喻嘗試讓你抓到些 functors 是如何運(yùn)作的感覺。在之后我們也會(huì)用相同的比喻來比喻 applicative functors 跟 monads。在多數(shù)情況下這種比喻是恰當(dāng)?shù)?,但不要過度引申,有些 functors 是不適用這個(gè)比喻的。一個(gè)比較正確的形容是 functors 是一個(gè)計(jì)算語境(computational context)。這個(gè)語境可能是這個(gè) computation 可能帶有值,或是有可能會(huì)失敗(像 ``Maybe`` 跟 ``Either a``),或是他可能有多個(gè)值(像 lists),等等。
如果一個(gè) type constructor 要是 Functor 的 instance,那他的 kind 必須是 * -> *,這代表他必須剛好接受一個(gè) type 當(dāng)作 type parameter。像是 Maybe 可以是 Functor 的一個(gè) instance,因?yàn)樗邮芤粋€(gè) type parameter,來做成像是 Maybe Int,或是 Maybe String。如果一個(gè) type constructor 接受兩個(gè)參數(shù),像是 Either,我們必須給他兩個(gè) type parameter。所以我們不能這樣寫:instance Functor Either where,但我們可以寫 instance Functor (Either a) where,如果我們把 fmap 限縮成只是 Either a 的,那他的型態(tài)就是 fmap :: (b -> c) -> Either a b -> Either a c。就像你看到的,Either a 的是固定的一部分,因?yàn)?nbsp;Either a 只恰好接受一個(gè) type parameter,但 Either 則要接受兩個(gè) type parameters。這樣 fmap 的型態(tài)變成 fmap :: (b -> c) -> Either b -> Either c,這不太合理。
我們知道有許多型態(tài)都是 Functor 的 instance,像是 [],Maybe,Either a 以及我們自己寫的 Tree。我們也看到了如何用一個(gè)函數(shù) map 他們。在這一章節(jié),我們?cè)俣嗯e兩個(gè)例子,也就是 IO 跟 (->) r。
如果一個(gè)值的型態(tài)是 IO String,他代表的是一個(gè)會(huì)被計(jì)算成 String 結(jié)果的 I/O action。我們可以用 do syntax 來把結(jié)果綁定到某個(gè)名稱。我們之前把 I/O action 比喻做長了腳的盒子,會(huì)到真實(shí)世界幫我們?nèi)∫恍┲祷貋?。我們可以查看他們?nèi)×耸裁粗担坏┛催^,我們必須要把值放回盒子中。用這個(gè)比喻,IO 的行為就像是一個(gè) functor。
我們來看看 IO 是怎么樣的一個(gè) Functor instance。當(dāng)我們 fmap 用一個(gè) function 來 map over I/O action 時(shí),我們會(huì)想要拿回一個(gè)裝著已經(jīng)用 function 映射過值的 I/O action。
instance Functor IO where
fmap f action = do
result <- action
return (f result)
對(duì)一個(gè) I/O action 做 map over 動(dòng)作的結(jié)果仍會(huì)是一個(gè) I/O action,所以我們才用 do syntax 來把兩個(gè) I/O action 黏成一個(gè)。在 fmap 的實(shí)作中,我們先執(zhí)行了原本傳進(jìn)的 I/O action,并把結(jié)果綁定成 result。然后我們寫了 return (f result)。return 就如你所知道的,是一個(gè)只會(huì)回傳包了你傳給他東西的 I/O action。還有一個(gè) do block 的回傳值一定是他最后一個(gè) I/O action 的回傳值。這也是為什么我們需要 return。其實(shí)他只是回傳包了 f result 的 I/O action。
我們可以再多實(shí)驗(yàn)一下來找到些感覺。來看看這段 code:
main = do line <- getLine
let line' = reverse line
putStrLn $ "You said " ++ line' ++ " backwards!"
putStrLn $ "Yes, you really said" ++ line' ++ " backwards!"
這程序要求用戶輸入一行文本,然后印出一行反過來的。 我們可以用 fmap 來改寫:
main = do line <- fmap reverse getLine
putStrLn $ "You said " ++ line ++ " backwards!"
putStrLn $ "Yes, you really said" ++ line ++ " backwards!"
就像我們用 fmap reverse 來 map over Just "blah" 會(huì)得到 Just "halb",我們也可以 fmap reverse 來 map over getLine。getLine 是一個(gè) I/O action,他的 type 是 IO String,而用 reverse 來 map over 他會(huì)回傳一個(gè)取回一個(gè)字串并 reverse 他的 I/O action。就像我們 apply 一個(gè) function 到一個(gè) Maybe 一樣,我們也可以 apply 一個(gè) function 到一個(gè) IO,只是這個(gè) IO 會(huì)跑去外面拿回某些值。然后我們把結(jié)果用 <- 綁定到某個(gè)名稱,而這個(gè)名稱綁定的值是已經(jīng) reverse 過了。
而 fmap (++"!") getLine 這個(gè) I/O action 表現(xiàn)得就像 getLine,只是他的結(jié)果多了一個(gè) "!" 在最后。
如果我們限縮 fmap 到 IO 型態(tài)上,那 fmap 的型態(tài)是 fmap :: (a -> b) -> IO a -> IO b。fmap 接受一個(gè)函數(shù)跟一個(gè) I/O action,并回傳一個(gè) I/O action 包含了已經(jīng) apply 過 function 的結(jié)果。
如果你曾經(jīng)注意到你想要將一個(gè) I/O action 綁定到一個(gè)名稱上,只是為了要 apply 一個(gè) function。你可以考慮使用 fmap,那會(huì)更漂亮地表達(dá)這件事。或者你想要對(duì) functor 中的數(shù)據(jù)做 transformation,你可以先將你要用的 function 寫在 top level,或是把他作成一個(gè) lambda function,甚至用 function composition。
import Data.Char
import Data.List
main = do line <- fmap (intersperse '-' . reverse . map toUpper) getLine
putStrLn line
$ runhaskell fmapping_io.hs
hello there
E-R-E-H-T- -O-L-L-E-H
正如你想的,intersperse '-' . reverse . map toUpper 合成了一個(gè) function,他接受一個(gè)字串,將他轉(zhuǎn)成大寫,然后反過來,再用 intersperse '-' 安插'-'。他是比較漂亮版本的 (\xs -> intersperse '-' (reverse (map toUpper xs)))。
另一個(gè) Functor 的案例是 (->) r,只是我們先前沒有注意到。你可能會(huì)困惑到底 (->) r 究竟代表什么?一個(gè) r -> a 的型態(tài)可以寫成 (->) r a,就像是 2 + 3 可以寫成 (+) 2 3 一樣。我們可以從一個(gè)不同的角度來看待 (->) r a,他其實(shí)只是一個(gè)接受兩個(gè)參數(shù)的 type constructor,好比 Either。但記住我們說過 Functor 只能接受一個(gè) type constructor。這也是為什么 (->) 不是 Functor 的一個(gè) instance,但 (->) r 則是。如果程序的語法允許的話,你也可以將 (->) r 寫成 (r ->)。就如 (2+) 代表的其實(shí)是 (+) 2。至于細(xì)節(jié)是如何呢?我們可以看看 Control.Monad.Instances。
我們通常說一個(gè)接受任何東西以及回傳隨便一個(gè)東西的函數(shù)型態(tài)是 ``a -> b``。``r -> a`` 是同樣意思,只是把符號(hào)代換了一下。
instance Functor ((->) r) where
fmap f g = (\x -> f (g x))
如果語法允許的話,他可以被寫成
instance Functor (r ->) where
fmap f g = (\x -> f (g x))
但其實(shí)是不允許的,所以我們必須寫成第一種的樣子。
首先我們來看看 fmap 的型態(tài)。他的型態(tài)是 fmap :: (a -> b) -> f a -> f b。我們把所有的 f 在心里代換成 (->) r。則 fmap 的型態(tài)就變成 fmap :: (a -> b) -> ((->) r a) -> ((->) r b)。接著我們把 (->) r a 跟 (->) r b 換成 r -> a 跟 r -> b。則我們得到 fmap :: (a -> b) -> (r -> a) -> (r -> b)。
從上面的結(jié)果看到將一個(gè) function map over 一個(gè) function 會(huì)得到另一個(gè) function,就如 map over 一個(gè) function 到 Maybe 會(huì)得到一個(gè) Maybe,而 map over 一個(gè) function 到一個(gè) list 會(huì)得到一個(gè) list。而 fmap :: (a -> b) -> (r -> a) -> (r -> b) 告訴我們什么?他接受一個(gè)從 a 到 b 的 function,跟一個(gè)從 r 到 a 的 function,并回傳一個(gè)從 r 到 b 的 function。這根本就是 function composition。把 r -> a 的輸出接到 a -> b 的輸入,的確是 function composition 在做的事。如果你再仔細(xì)看看 instance 的定義,會(huì)發(fā)現(xiàn)真的就是一個(gè) function composition。
instance Functor ((->) r) where
fmap = (.)
這很明顯就是把 fmap 當(dāng) composition 在用。可以用 :m + Control.Monad.Instances 把模塊裝載進(jìn)來,并做一些嘗試。
ghci> :t fmap (*3) (+100)
fmap (*3) (+100) :: (Num a) => a -> a
ghci> fmap (*3) (+100) 1
303
ghci> (*3) `fmap` (+100) $ 1
303
ghci> (*3) . (+100) $ 1
303
ghci> fmap (show . (*3)) (*100) 1
"300"
我們調(diào)用 fmap 的方式是 infix 的方式,這跟 . 很像。在第二行,我們把 (*3) map over 到 (+100) 上,這會(huì)回傳一個(gè)先把輸入值 (+100) 再 (*3) 的 function,我們?cè)儆?nbsp;1 去調(diào)用他。
到這邊為止盒子的比喻還適用嗎?如果你硬是要解釋的話還是解釋得通。當(dāng)我們將 fmap (+3) map over Just 3 的時(shí)候,對(duì)于 Maybe 我們很容易把他想成是裝了值的盒子,我們只是對(duì)盒子里面的值 (+3)。但對(duì)于 fmap (*3) (+100) 呢?你可以把 (+100) 想成是一個(gè)裝了值的盒子。有點(diǎn)像把 I/O action 想成長了腳的盒子一樣。對(duì) (+100) 使用 fmap (*3) 會(huì)產(chǎn)生另一個(gè)表現(xiàn)得像 (+100) 的 function。只是在算出值之前,會(huì)再多計(jì)算 (*3)。這樣我們可以看出來 fmap 表現(xiàn)得就像 . 一樣。
fmap 等同于 function composition 這件事對(duì)我們來說并不是很實(shí)用,但至少是一個(gè)有趣的觀點(diǎn)。這也讓我們打開視野,看到盒子的比喻不是那么恰當(dāng),functors 其實(shí)比較像 computation。function 被 map over 到一個(gè) computation 會(huì)產(chǎn)生經(jīng)由那個(gè) function 映射過后的 computation。
在我們繼續(xù)看 fmap 該遵守的規(guī)則之前,我們?cè)倏匆淮?nbsp;fmap 的型態(tài),他是 fmap :: (a -> b) -> f a -> f b。很明顯我們是在討論 Functor,所以為了簡潔,我們就不寫 (Functor f) => 的部份。當(dāng)我們?cè)趯W(xué) curry 的時(shí)候,我們說過 Haskell 的 function 實(shí)際上只接受一個(gè)參數(shù)。一個(gè)型態(tài)是 a -> b -> c 的函數(shù)實(shí)際上是接受 a 然后回傳 b -> c,而 b -> c 實(shí)際上接受一個(gè) b 然后回傳一個(gè) c。如果我們用比較少的參數(shù)調(diào)用一個(gè)函數(shù),他就會(huì)回傳一個(gè)函數(shù)需要接受剩下的參數(shù)。所以 a -> b -> c 可以寫成 a -> (b -> c)。這樣 curry 可以明顯一些。
同樣的,我們可以不要把 fmap 想成是一個(gè)接受 function 跟 functor 并回傳一個(gè) function 的 function。而是想成一個(gè)接受 function 并回傳一個(gè)新的 function 的 function,回傳的 function 接受一個(gè) functor 并回傳一個(gè) functor。他接受 a -> b 并回傳 f a -> f b。這動(dòng)作叫做 lifting。我們用 GHCI 的 :t 來做的實(shí)驗(yàn)。
ghci> :t fmap (*2)
fmap (*2) :: (Num a, Functor f) => f a -> f a
ghci> :t fmap (replicate 3)
fmap (replicate 3) :: (Functor f) => f a -> f [a]
fmap (*2) 接受一個(gè) functor f,并回傳一個(gè)基于數(shù)字的 functor。那個(gè) functor 可以是 list,可以是 Maybe,可以是 Either String。fmap (replicate 3) 可以接受一個(gè)基于任何型態(tài)的 functor,并回傳一個(gè)基于 list 的 functor。
當(dāng)我們提到 functor over numbers 的時(shí)候,你可以想像他是一個(gè) functor 包含有許多數(shù)字在里面。前面一種說法其實(shí)比較正確,但后面一種說法比較容易讓人理解。
這樣的觀察在我們只有綁定一個(gè)部份套用的函數(shù),像是 fmap (++"!"),的時(shí)候會(huì)顯得更清楚,
你可以把 fmap 想做是一個(gè)函數(shù),他接受另一個(gè)函數(shù)跟一個(gè) functor,然后把函數(shù)對(duì) functor 每一個(gè)元素做映射,或你可以想做他是一個(gè)函數(shù),他接受一個(gè)函數(shù)并把他 lift 到可以在 functors 上面操作。兩種想法都是正確的,而且在 Haskell 中是等價(jià)。
fmap (replicate 3) :: (Functor f) => f a -> f [a] 這樣的型態(tài)代表這個(gè)函數(shù)可以運(yùn)作在任何 functor 上。至于確切的行為則要看究竟我們操作的是什么樣的 functor。如果我們是用 fmap (replicate 3) 對(duì)一個(gè) list 操作,那我們會(huì)選擇 fmap 針對(duì) list 的實(shí)作,也就是只是一個(gè) map。如果我們是碰到 Maybe a。那他在碰到 Just 型態(tài)的時(shí)候,會(huì)對(duì)里面的值套用 replicate 3。而碰到 Nothing 的時(shí)候就回傳 Nothing。
ghci> fmap (replicate 3) [1,2,3,4]
[[1,1,1],[2,2,2],[3,3,3],[4,4,4]]
ghci> fmap (replicate 3) (Just 4)
Just [4,4,4]
ghci> fmap (replicate 3) (Right "blah")
Right ["blah","blah","blah"]
ghci> fmap (replicate 3) Nothing
Nothing
ghci> fmap (replicate 3) (Left "foo")
Left "foo"
接下來我們來看看 functor laws。一個(gè)東西要成為 functor,必須要遵守某些定律。不管任何一個(gè) functor 都被要求具有某些性質(zhì)。他們必須是能被 map over 的。對(duì)他們調(diào)用 fmap 應(yīng)該是要用一個(gè)函數(shù) map 每一個(gè)元素,不多做任何事情。這些行為都被 functor laws 所描述。對(duì)于 Functor 的 instance 來說,總共兩條定律應(yīng)該被遵守。不過他們不會(huì)在 Haskell 中自動(dòng)被檢查,所以你必須自己確認(rèn)這些條件。
functor law 的第一條說明,如果我們對(duì) functor 做 map id,那得到的新的 functor 應(yīng)該要跟原來的一樣。如果寫得正式一點(diǎn),他代表 fmap id = id?;旧纤褪钦f對(duì) functor 調(diào)用 fmap id,應(yīng)該等同于對(duì) functor 調(diào)用 id 一樣。畢竟 id 只是 identity function,他只會(huì)把參數(shù)照原樣丟出。他也可以被寫成 \x -> x。如果我們對(duì) functor 的概念就是可以被 map over 的對(duì)象,那 fmap id = id 的性就顯而易見。
我們來看看這個(gè)定律的幾個(gè)案例:
ghci> fmap id (Just 3)
Just 3
ghci> id (Just 3)
Just 3
ghci> fmap id [1..5]
[1,2,3,4,5]
ghci> id [1..5]
[1,2,3,4,5]
ghci> fmap id []
[]
ghci> fmap id Nothing
Nothing
如果我們看看 Maybe 的 fmap 的實(shí)作,我們不難發(fā)現(xiàn)第一定律為何被遵守。
instance Functor Maybe where
fmap f (Just x) = Just (f x)
fmap f Nothing = Nothing
我們可以想像在 f 的位置擺上 id。我們看到 fmap id 拿到 Just x 的時(shí)候,結(jié)果只不過是 Just (id x),而 id 有只回傳他拿到的東西,所以可以知道 Just (id x) 等價(jià)于 Just x。所以說我們可以知道對(duì) Maybe 中的 Just 用 id 去做 map over 的動(dòng)作,會(huì)拿回一樣的值。
而將 id map over Nothing 會(huì)拿回 Nothing 并不稀奇。所以從這兩個(gè) fmap 的實(shí)作,我們可以看到的確 fmap id = id 有被遵守。
*第二定律描述說先將兩個(gè)函數(shù)合成并將結(jié)果 map over 一個(gè) functor 的結(jié)果,應(yīng)該跟先將第一個(gè)函數(shù) map over 一個(gè) functor,再將第二個(gè)函數(shù) map over 那個(gè) functor 的結(jié)果是一樣的。*正式地寫下來的話就是 fmap (f . g) = fmap f . fmap g。或是用另外一種寫法,對(duì)于任何一個(gè) functor F,下面這個(gè)式子應(yīng)該要被遵守:fmap (f . g) F = fmap f (fmap g F)。
如果我們能夠證明某個(gè)型別遵守兩個(gè)定律,那我們就可以保證他跟其他 functor 對(duì)于映射方面都擁有相同的性質(zhì)。我們知道如果對(duì)他用 fmap,我們知道不會(huì)有除了 mapping 以外的事會(huì)發(fā)生,而他就僅僅會(huì)表現(xiàn)成某個(gè)可以被 map over 的東西。也就是一個(gè) functor。你可以再仔細(xì)查看 fmap 對(duì)于某些型別的實(shí)作來了解第二定律。正如我們先前對(duì) Maybe 查看第一定律一般。
如果你需要的話,我們能在這邊演練一下 Maybe 是如何遵守第二定律的。首先 fmap (f . g) 來 map over Nothing 的話,我們會(huì)得到 Nothing。因?yàn)橛萌魏魏瘮?shù)來 fmap Nothing 的話都會(huì)回傳 Nothing。如果我們 fmap f (fmap g Nothing),我們會(huì)得到 Nothing??梢钥吹疆?dāng)面對(duì) Nothing 的時(shí)候,Maybe 很顯然是遵守第二定律的。 那對(duì)于 Just something 呢?如果我們使用 fmap (f . g) (Just x) 的話,從實(shí)作的代碼中我可以看到 Just ((f . g ) x),也就是 Just (f (g x))。如果我們使用 fmap f (fmap g (Just x)) 的話我們可以從實(shí)作知道 fmap g (Just x) 會(huì)是 Just (g x)。fmap f (fmap g (Just x)) 跟 fmap f (Just (g x)) 相等。而從實(shí)作上這又會(huì)相等于 Just (f (g x))。
如果你不太理解這邊的說明,別擔(dān)心。只要確定你了解什么是函數(shù)合成就好。在多數(shù)的情況下你可以直覺地對(duì)應(yīng)到這些型別表現(xiàn)得就像 containers 或函數(shù)一樣。或是也可以換種方法,只要多嘗試對(duì)型別中不同的值做操作你就可以看看型別是否有遵守定律。
我們來看一些經(jīng)典的例子。這些型別建構(gòu)子雖然是 Functor 的 instance,但實(shí)際上他們并不是 functor,因?yàn)樗麄儾⒉蛔袷剡@些定律。我們來看看其中一個(gè)型別。
data CMaybe a = CNothing | CJust Int a deriving (Show)
C 這邊代表的是計(jì)數(shù)器。他是一種看起來像是 Maybe a 的型別,只差在 Just 包含了兩個(gè) field 而不是一個(gè)。在 CJust 中的第一個(gè) field 是 Int,他是扮演計(jì)數(shù)器用的。而第二個(gè) field 則為型別 a,他是從型別參數(shù)來的,而他確切的型別當(dāng)然會(huì)依據(jù)我們選定的 CMaybe a 而定。我們來對(duì)他作些操作來獲得些操作上的直覺吧。
ghci> CNothing
CNothing
ghci> CJust 0 "haha"
CJust 0 "haha"
ghci> :t CNothing
CNothing :: CMaybe a
ghci> :t CJust 0 "haha"
CJust 0 "haha" :: CMaybe [Char]
ghci> CJust 100 [1,2,3]
CJust 100 [1,2,3]
如果我們使用 CNothing,就代表不含有 field。如果我們用的是 CJust,那第一個(gè) field 是整數(shù),而第二個(gè) field 可以為任何型別。我們來定義一個(gè) Functor 的 instance,這樣每次我們使用 fmap 的時(shí)候,函數(shù)會(huì)被套用在第二個(gè) field,而第一個(gè) field 會(huì)被加一。
instance Functor CMaybe where
fmap f CNothing = CNothing
fmap f (CJust counter x) = CJust (counter+1) (f x)
這種定義方式有點(diǎn)像是 Maybe 的定義方式,只差在當(dāng)我們使用 fmap 的時(shí)候,如果碰到的不是空值,那我們不只會(huì)套用函數(shù),還會(huì)把計(jì)數(shù)器加一。我們可以來看一些范例操作。
ghci> fmap (++"ha") (CJust 0 "ho")
CJust 1 "hoha"
ghci> fmap (++"he") (fmap (++"ha") (CJust 0 "ho"))
CJust 2 "hohahe"
ghci> fmap (++"blah") CNothing
CNothing
這些會(huì)遵守 functor laws 嗎?要知道有不遵守的情形,只要找到一個(gè)反例就好了。
ghci> fmap id (CJust 0 "haha")
CJust 1 "haha"
ghci> id (CJust 0 "haha")
CJust 0 "haha"
我們知道 functor law 的第一定律描述當(dāng)我們用 id 來 map over 一個(gè) functor 的時(shí)候,他的結(jié)果應(yīng)該跟只對(duì) functor 調(diào)用 id 的結(jié)果一樣。但我們可以看到這個(gè)例子中,這對(duì)于 CMaybe 并不遵守。盡管他的確是 Functor typeclass 的一個(gè) instance。但他并不遵守 functor law 因此不是一個(gè) functor。如果有人使用我們的 CMaybe 型別,把他當(dāng)作 functor 用,那他就會(huì)期待 functor laws 會(huì)被遵守。但 CMaybe 并沒辦法滿足,便會(huì)造成錯(cuò)誤的程序。當(dāng)我們使用一個(gè) functor 的時(shí)候,函數(shù)合成跟 map over 的先后順序不應(yīng)該有影響。但對(duì)于 CMaybe 他是有影響的,因?yàn)樗o(jì)錄了被 map over 的次數(shù)。如果我們希望 CMaybe 遵守 functor law,我們必須要讓 Int 字段在做 fmap 的時(shí)候維持不變。
乍看之下 functor laws 看起來不是很必要,也容易讓人搞不懂,但我們知道如果一個(gè)型別遵守 functor laws,那我們就能對(duì)他作些基本的假設(shè)。如果遵守了 functor laws,我們知道對(duì)他做 fmap 不會(huì)做多余的事情,只是用一個(gè)函數(shù)做映射而已。這讓寫出來的代碼足夠抽象也容易擴(kuò)展。因?yàn)槲覀兛梢杂枚蓙硗普撔蛣e的行為。
所有在標(biāo)準(zhǔn)函式庫中的 Functor 的 instance 都遵守這些定律,但你可以自己檢查一遍。下一次你定義一個(gè)型別為 Functor 的 instance 的時(shí)候,花點(diǎn)時(shí)間確認(rèn)他確實(shí)遵守 functor laws。一旦你操作過足夠多的 functors 時(shí),你就會(huì)獲得直覺,知道他們會(huì)有什么樣的性質(zhì)跟行為。而且 functor laws 也會(huì)覺得顯而易見。但就算沒有這些直覺,你仍然可以一行一行地來找看看有沒有反例讓這些定律失效。
我們可以把 functor 看作輸出具有 context 的值。例如說 Just 3 就是輸出 3,但他又帶有一個(gè)可能沒有值的 context。[1,2,3] 輸出三個(gè)值,1,2 跟 3,同時(shí)也帶有可能有多個(gè)值或沒有值的 context。(+3) 則會(huì)帶有一個(gè)依賴于參數(shù)的 context。
如果你把 functor 想做是輸出值這件事,那你可以把 map over 一個(gè) functor 這件事想成在 functor 輸出的后面再多加一層轉(zhuǎn)換。當(dāng)我們做 fmap (+3) [1,2,3] 的時(shí)候,我們是把 (+3) 接到 [1,2,3] 后面,所以當(dāng)我們查看任何一個(gè) list 的輸出的時(shí)候,(+3) 也會(huì)被套用在上面。另一個(gè)例子是對(duì)函數(shù)做 map over。當(dāng)我們做 fmap (+3) (*3),我們是把 (+3) 這個(gè)轉(zhuǎn)換套用在 (*3) 后面。這樣想的話會(huì)很自然就會(huì)把 fmap 跟函數(shù)合成關(guān)聯(lián)起來(fmap (+3) (*3) 等價(jià)于 (+3) . (*3),也等價(jià)于 \x -> ((x*3)+3)),畢竟我們是接受一個(gè)函數(shù) (*3) 然后套用 (+3) 轉(zhuǎn)換。最后的結(jié)果仍然是一個(gè)函數(shù),只是當(dāng)我們喂給他一個(gè)數(shù)字的時(shí)候,他會(huì)先乘上三然后做轉(zhuǎn)換加上三。這基本上就是函數(shù)合成在做的事。
在這個(gè)章節(jié)中,我們會(huì)學(xué)到 applicative functors,也就是加強(qiáng)版的 functors,在 Haskell 中是用在 Control.Applicative 中的 Applicative 這個(gè) typeclass 來定義的。
你還記得 Haskell 中函數(shù)缺省就是 Curried 的,那代表接受多個(gè)參數(shù)的函數(shù)實(shí)際上是接受一個(gè)參數(shù)然后回傳一個(gè)接受剩余參數(shù)的函數(shù),以此類推。如果一個(gè)函數(shù)的型別是 a -> b -> c,我們通常會(huì)說這個(gè)函數(shù)接受兩個(gè)參數(shù)并回傳 c,但他實(shí)際上是接受 a 并回傳一個(gè) b -> c 的函數(shù)。這也是為什么我們可以用 (f x) y 的方式調(diào)用 f x y。這個(gè)機(jī)制讓我們可以 partially apply 一個(gè)函數(shù),可以用比較少的參數(shù)調(diào)用他們??梢宰龀梢粋€(gè)函數(shù)再喂給其他函數(shù)。
到目前為止,當(dāng)我們要對(duì) functor map over 一個(gè)函數(shù)的時(shí)候,我們用的函數(shù)都是只接受一個(gè)參數(shù)的。但如果我們要 map 一個(gè)接受兩個(gè)參數(shù)的函數(shù)呢?我們來看幾個(gè)具體的例子。如果我們有 Just 3 然后我們做 fmap (*) (Just 3),那我們會(huì)獲得什么樣的結(jié)果?從 Maybe 對(duì) Functor 的 instance 實(shí)作來看,我們知道如果他是 Just something,他會(huì)對(duì)在 Just 中的 something 做映射。因此當(dāng) fmap (*) (Just 3) 會(huì)得到 Just ((*) 3),也可以寫做 Just (* 3)。我們得到了一個(gè)包在 Just 中的函數(shù)。
ghci> :t fmap (++) (Just "hey")
fmap (++) (Just "hey") :: Maybe ([Char] -> [Char])
ghci> :t fmap compare (Just 'a')
fmap compare (Just 'a') :: Maybe (Char -> Ordering)
ghci> :t fmap compare "A LIST OF CHARS"
fmap compare "A LIST OF CHARS" :: [Char -> Ordering]
ghci> :t fmap (\x y z -> x + y / z) [3,4,5,6]
fmap (\x y z -> x + y / z) [3,4,5,6] :: (Fractional a) => [a -> a -> a]
如果我們 map compare 到一個(gè)包含許多字符的 list 呢?他的型別是 (Ord a) => a -> a -> Ordering,我們會(huì)得到包含許多 Char -> Ordering 型別函數(shù)的 list,因?yàn)?nbsp;compare 被 partially apply 到 list 中的字符。他不是包含許多 (Ord a) => a -> Ordering 的函數(shù),因?yàn)榈谝粋€(gè) a 碰到的型別是 Char,所以第二個(gè) a 也必須是 Char。
我們看到如何用一個(gè)多參數(shù)的函數(shù)來 map functor,我們會(huì)得到一個(gè)包含了函數(shù)的 functor。那現(xiàn)在我們能對(duì)這個(gè)包含了函數(shù)的 functor 做什么呢?我們能用一個(gè)吃這些函數(shù)的函數(shù)來 map over 這個(gè) functor,這些在 functor 中的函數(shù)都會(huì)被當(dāng)作參數(shù)丟給我們的函數(shù)。
ghci> let a = fmap (*) [1,2,3,4]
ghci> :t a
a :: [Integer -> Integer]
ghci> fmap (\f -> f 9) a
[9,18,27,36]
但如果我們的有一個(gè) functor 里面是 Just (3 *) 還有另一個(gè) functor 里面是 Just 5,但我們想要把第一個(gè) Just (3 *) map over Just 5 呢?如果是普通的 functor,那就沒救了。因?yàn)樗麄冎辉试S map 一個(gè)普通的函數(shù)。即使我們用 \f -> f 9 來 map 一個(gè)裝了很多函數(shù)的 functor,我們也是使用了普通的函數(shù)。我們是無法單純用 fmap 來把包在一個(gè) functor 的函數(shù) map 另一個(gè)包在 functor 中的值。我們能用模式匹配 Just 來把函數(shù)從里面抽出來,然后再 map Just 5,但我們是希望有一個(gè)一般化的作法,對(duì)任何 functor 都有效。
我們來看看 Applicative 這個(gè) typeclass。他位在 Control.Applicative 中,在其中定義了兩個(gè)函數(shù) pure 跟 <*>。他并沒有提供缺省的實(shí)作,如果我們想使用他必須要為他們 applicative functor 的實(shí)作。typeclass 定義如下:
class (Functor f) => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
這簡簡單單的三行可以讓我們學(xué)到不少。首先來看第一行。他開啟了 Applicative 的定義,并加上 class contraint。描述了一個(gè)型別構(gòu)造子要是 Applicative,他必須也是 Functor。這就是為什么我們說一個(gè)型別構(gòu)造子屬于 Applicative 的話,他也會(huì)是 Functor,因此我們能對(duì)他使用 fmap。
第一個(gè)定義的是 pure。他的型別宣告是 pure :: a -> f a。f 代表 applicative functor 的 instance。由于 Haskell 有一個(gè)優(yōu)秀的型別系統(tǒng),其中函數(shù)又是將一些參數(shù)映射成結(jié)果,我們可以從型別宣告中讀出許多消息。pure 應(yīng)該要接受一個(gè)值,然后回傳一個(gè)包含那個(gè)值的 applicative functor。我們這邊是用盒子來作比喻,即使有一些比喻不完全符合現(xiàn)實(shí)的情況。盡管這樣,a -> f a 仍有許多豐富的信息,他確實(shí)告訴我們他會(huì)接受一個(gè)值并回傳一個(gè) applicative functor,里面裝有結(jié)果。
對(duì)于 pure 比較好的說法是把一個(gè)普通值放到一個(gè)缺省的 context 下,一個(gè)最小的 context 但仍然包含這個(gè)值。
<*> 也非常有趣。他的型別是 f (a -> b) -> f a -> f b。這有讓你聯(lián)想到什么嗎?沒錯(cuò)!就是 fmap :: (a -> b) -> f a -> f b。他有點(diǎn)像加強(qiáng)版的 fmap。然而 fmap 接受一個(gè)函數(shù)跟一個(gè) functor,然后套用 functor 之中的函數(shù)。<*> 則是接受一個(gè)裝有函數(shù)的 functor 跟另一個(gè) functor,然后取出第一個(gè) functor 中的函數(shù)將他對(duì)第二個(gè) functor 中的值做 map。
我們來看看 Maybe 的 Applicative 實(shí)作:
instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing
(Just f) <*> something = fmap f something
從 class 的定義我們可以看到 f 作為 applicative functor 會(huì)接受一個(gè)具體型別當(dāng)作參數(shù),所以我們是寫成 instance Applicative Maybe where 而不是寫成 instance Applicative (Maybe a) where。
首先看到 pure。他只不過是接受一個(gè)東西然后包成 applicative functor。我們寫成 pure = Just 是因?yàn)?nbsp;Just 不過就是一個(gè)普通函數(shù)。我們其實(shí)也可以寫成 pure x = Just x。
接著我們定義了 <*>。我們無法從 Nothing 中抽出一個(gè)函數(shù),因?yàn)?nbsp;Nothing 并不包含一個(gè)函數(shù)。所以我們說如果我們要嘗試從 Nothing 中取出一個(gè)函數(shù),結(jié)果必定是 Nothing。如果你看看 Applicative 的定義,你會(huì)看到他有 Functor 的限制,他代表 <*> 的兩個(gè)參數(shù)都會(huì)是 functors。如果第一個(gè)參數(shù)不是 Nothing,而是一個(gè)裝了函數(shù)的 Just,而且我們希望將這個(gè)函數(shù)對(duì)第二個(gè)參數(shù)做 map。這個(gè)也考慮到第二個(gè)參數(shù)是 Nothing 的情況,因?yàn)?nbsp;fmap 任何一個(gè)函數(shù)至 Nothing 會(huì)回傳 Nothing。
對(duì)于 Maybe 而言,如果左邊是 Just,那 <*> 會(huì)從其中抽出了一個(gè)函數(shù)來 map 右邊的值。如果有任何一個(gè)參數(shù)是 Nothing。那結(jié)果便是 Nothing。
來試試看吧!
ghci> Just (+3) <*> Just 9
Just 12
ghci> pure (+3) <*> Just 10
Just 13
ghci> pure (+3) <*> Just 9
Just 12
ghci> Just (++"hahah") <*> Nothing
Nothing
ghci> Nothing <*> Just "woot"
Nothing
我們看到 pure (+3) 跟 Just (+3) 在這個(gè) case 下是一樣的。如果你是在 applicative context 底下跟 Maybe 打交道的話請(qǐng)用 pure,要不然就用 Just。前四個(gè)輸入展示了函數(shù)是如何被取出并做 map 的動(dòng)作,但在這個(gè) case 底下,他們同樣也可以用 unwrap 函數(shù)來 map over functors。最后一行比較有趣,因?yàn)槲覀冊(cè)囍鴱?nbsp;Nothing 取出函數(shù)并將他 map 到某個(gè)值。結(jié)果當(dāng)然是 Nothing。
對(duì)于普通的 functors,你可以用一個(gè)函數(shù) map over 一個(gè) functors,但你可能沒辦法拿到結(jié)果。而 applicative functors 則讓你可以用單一一個(gè)函數(shù)操作好幾個(gè) functors??纯聪旅嬉欢未a:
ghci> pure (+) <*> Just 3 <*> Just 5
Just 8
ghci> pure (+) <*> Just 3 <*> Nothing
Nothing
ghci> pure (+) <*> Nothing <*> Just 5
Nothing
究竟我們寫了些什么?我們來一步步看一下。<*> 是 left-associative,也就是說 pure (+) <*> Just 3 <*> Just 5 可以寫成 (pure (+) <*> Just 3) <*> Just 5。首先 + 是擺在一個(gè) functor 中,在這邊剛好他是一個(gè) Maybe。所以首先,我們有 pure (+),他等價(jià)于 Just (+)。接下來由于 partial application 的關(guān)系,Just (+) <*> Just 3 等價(jià)于 Just (3+)。把一個(gè) 3 喂給 + 形成另一個(gè)只接受一個(gè)參數(shù)的函數(shù),他的效果等于加上 3。最后 Just (3+) <*> Just 5 被運(yùn)算,其結(jié)果是 Just 8。
這樣很棒吧!用 applicative style 的方式來使用 applicative functors。像是 pure f <*> x <*> y <*> ... 就讓我們可以拿一個(gè)接受多個(gè)參數(shù)的函數(shù),而且這些參數(shù)不一定是被包在 functor 中。就這樣來套用在多個(gè)在 functor context 的值。這個(gè)函數(shù)可以吃任意多的參數(shù),畢竟 <*> 只是做 partial application 而已。
如果我們考慮到 pure f <*> x 等于 fmap f x 的話,這樣的用法就更方便了。這是 applicative laws 的其中一條。我們稍后會(huì)更仔細(xì)地查看這條定律。現(xiàn)在我們先依直覺來使用他。就像我們先前所說的,pure 把一個(gè)值放進(jìn)一個(gè)缺省的 context 中。如果我們要把一個(gè)函數(shù)放在一個(gè)缺省的 context,然后把他取出并套用在放在另一個(gè) applicative functor 的值。我們會(huì)做的事就是把函數(shù) map over 那個(gè) applicative functor。但我們不會(huì)寫成 pure f <*> x <*> y <*> ...,而是寫成 fmap f x <*> y <*> ...。這也是為什么 Control.Applicative 會(huì) export 一個(gè)函數(shù) <$>,他基本上就是中綴版的 fmap。他是這么被定義的:
(<$>) :: (Functor f) => (a -> b) -> f a -> f b
f <$> x = fmap f x
要記住型別變量跟參數(shù)的名字還有值綁定的名稱不沖突。``f`` 在函數(shù)的型別宣告中是型別變量,說明 ``f`` 應(yīng)該要滿足 ``Functor`` typeclass 的條件。而在函數(shù)本體中的 ``f`` 則表示一個(gè)函數(shù),我們將他 map over x。我們同樣用 ``f`` 來表示他們并代表他們是相同的東西。
<$> 的使用顯示了 applicative style 的好處。如果我們想要將 f 套用三個(gè) applicative functor。我們可以寫成 f <$> x <*> y <*> z。如果參數(shù)不是 applicative functor 而是普通值的話。我們則寫成 f x y z。
我們?cè)僮屑?xì)看看他是如何運(yùn)作的。我們有一個(gè) Just "johntra" 跟 Just "volta" 這樣的值,我們希望將他們結(jié)合成一個(gè) String,并且包含在 Maybe 中。我們會(huì)這樣做:
ghci> (++) <$> Just "johntra" <*> Just "volta"
Just "johntravolta"
可以將上面的跟下面這行比較一下:
ghci> (++) "johntra" "volta"
"johntravolta"
可以將一個(gè)普通的函數(shù)套用在 applicative functor 上真不錯(cuò)。只要稍微寫一些 <$> 跟 <*> 就可以把函數(shù)變成 applicative style,可以操作 applicatives 并回傳 applicatives。
總之當(dāng)我們?cè)谧?nbsp;(++) <$> Just "johntra" <*> Just "volta" 時(shí),首先我們將 (++) map over 到 Just "johntra",然后產(chǎn)生 Just ("johntra"++),其中 (++) 的型別為 (++) :: [a] -> [a] -> [a],Just ("johntra"++) 的型別為 Maybe ([Char] -> [Char])。注意到 (++) 是如何吃掉第一個(gè)參數(shù),以及我們是怎么決定 a 是 Char 的。當(dāng)我們做 Just ("johntra"++) <*> Just "volta",他接受一個(gè)包在 Just 中的函數(shù),然后 map over Just "volta",產(chǎn)生了 Just "johntravolta"。如果兩個(gè)值中有任意一個(gè)為 Nothing,那整個(gè)結(jié)果就會(huì)是 Nothing。
到目前為止我們只有用 Maybe 當(dāng)作我們的案例,你可能也會(huì)想說 applicative functor 差不多就等于 Maybe。不過其實(shí)有許多其他 Applicative 的 instance。我們來看看有哪些。
List 也是 applicative functor。很驚訝嗎?來看看我們是怎么定義 [] 為 Applicative 的 instance 的。
instance Applicative [] where
pure x = [x]
fs <*> xs = [f x | f <- fs, x <- xs]
早先我們說過 pure 是把一個(gè)值放進(jìn)缺省的 context 中。換種說法就是一個(gè)會(huì)產(chǎn)生那個(gè)值的最小 context。而對(duì) list 而言最小 context 就是 [],但由于空的 list 并不包含一個(gè)值,所以我們沒辦法把他當(dāng)作 pure。這也是為什么 pure 其實(shí)是接受一個(gè)值然后回傳一個(gè)包含單元素的 list。同樣的,Maybe 的最小 context 是 Nothing,但他其實(shí)表示的是沒有值。所以 pure 其實(shí)是被實(shí)作成 Just 的。
ghci> pure "Hey" :: [String]
["Hey"]
ghci> pure "Hey" :: Maybe String
Just "Hey"
至于 <*> 呢?如果我們假定 <*> 的型別是限制在 list 上的話,我們會(huì)得到 (<*>) :: [a -> b] -> [a] -> [b]。他是用 list comprehension 來實(shí)作的。<*> 必須要從左邊的參數(shù)取出函數(shù),將他 map over 右邊的參數(shù)。但左邊的 list 有可能不包含任何函數(shù),也可能包含一個(gè)函數(shù),甚至是多個(gè)函數(shù)。而右邊的 list 有可能包含多個(gè)值。這也是為什么我們用 list comprehension 的方式來從兩個(gè) list 取值。我們要對(duì)左右任意的組合都做套用的動(dòng)作。而得到的結(jié)果就會(huì)是左右兩者任意組合的結(jié)果。
ghci> [(*0),(+100),(^2)] <*> [1,2,3]
[0,0,0,101,102,103,1,4,9]
左邊的 list 包含三個(gè)函數(shù),而右邊的 list 有三個(gè)值。所以結(jié)果會(huì)是有九個(gè)元素的 list。在左邊 list 中的每一個(gè)函數(shù)都被套用到右邊的值。如果我們今天在 list 中的函數(shù)是接收兩個(gè)參數(shù)的,我們也可以套用到兩個(gè) list 上。
ghci> [(+),(*)] <*> [1,2] <*> [3,4]
[4,5,5,6,3,4,6,8]
由于 <*> 是 left-associative,也就是說 [(+),(*)] <*> [1,2] 會(huì)先運(yùn)作,產(chǎn)生 [(1+),(2+),(1*),(2*)]。由于左邊的每一個(gè)函數(shù)都套用至右邊的每一個(gè)值。也就產(chǎn)生 [(1+),(2+),(1*),(2*)] <*> [3,4],其便是最終結(jié)果。
list 的 applicative style 是相當(dāng)有趣的:
ghci> (++) <$> ["ha","heh","hmm"] <*> ["?","!","."]
["ha?","ha!","ha.","heh?","heh!","heh.","hmm?","hmm!","hmm."]
看看我們是如何將一個(gè)接受兩個(gè)字串參數(shù)的函數(shù)套用到兩個(gè) applicative functor 上的,只要用適當(dāng)?shù)?applicative 運(yùn)算子就可以達(dá)成。
你可以將 list 看作是一個(gè) non-deterministic 的計(jì)算。而對(duì)于像 100 或是 "what" 這樣的值則是 deterministic 的計(jì)算,只會(huì)有一個(gè)結(jié)果。而 [1,2,3] 則可以看作是沒有確定究竟是哪一種結(jié)果。所以他代表的是所有可能的結(jié)果。當(dāng)你在做 (+) <$> [1,2,3] <*> [4,5,6],你可以想做是把兩個(gè) non-deterministic 的計(jì)算做 +,只是他會(huì)產(chǎn)生另一個(gè) non-deterministic 的計(jì)算,而且結(jié)果更加不確定。
Applicative style 對(duì)于 list 而言是一個(gè)取代 list comprehension 的好方式。在第二章中,我們想要看到 [2,5,10] 跟 [8,10,11] 相乘的結(jié)果,所以我們這樣做:
ghci> [ x*y | x <- [2,5,10], y <- [8,10,11]]
[16,20,22,40,50,55,80,100,110]
我們只是從兩個(gè) list 中取出元素,并將一個(gè)函數(shù)套用在任何元素的組合上。這也可以用 applicative style 的方式來寫:
ghci> (*) <$> [2,5,10] <*> [8,10,11]
[16,20,22,40,50,55,80,100,110]
這寫法對(duì)我來說比較清楚。可以清楚表達(dá)我們是要對(duì)兩個(gè) non-deterministic 的計(jì)算做 *。如果我們想要所有相乘大于 50 可能的計(jì)算結(jié)果,我們會(huì)這樣寫:
ghci> filter (>50) $ (*) <$> [2,5,10] <*> [8,10,11]
[55,80,100,110]
很容易看到 pure f <*> xs 等價(jià)于 fmap f xs。而 pure f 就是 [f],而且 [f] <*> xs 可將左邊的每個(gè)函數(shù)套用至右邊的每個(gè)值。但左邊其實(shí)只有一個(gè)函數(shù),所以他做起來就像是 mapping。
另一個(gè)我們已經(jīng)看過的 Applicative 的 instance 是 IO,來看看他是怎么實(shí)作的:
instance Applicative IO where
pure = return
a <*> b = do
f <- a
x <- b
return (f x)
由于 pure 是把一個(gè)值放進(jìn)最小的 context 中,所以將 return 定義成 pure 是很合理的。因?yàn)?nbsp;return 也是做同樣的事情。他做了一個(gè)不做任何事情的 I/O action,他可以產(chǎn)生某些值來作為結(jié)果,但他實(shí)際上并沒有做任何 I/O 的動(dòng)作,例如說印出結(jié)果到終端或是文件。
如果 <*> 被限定在 IO 上操作的話,他的型別會(huì)是 (<*>) :: IO (a -> b) -> IO a -> IO b。他接受一個(gè)產(chǎn)生函數(shù)的 I/O action,還有另一個(gè) I/O action,并從以上兩者創(chuàng)造一個(gè)新的 I/O action,也就是把第二個(gè)參數(shù)喂給第一個(gè)參數(shù)。而得到回傳的結(jié)果,然后放到新的 I/O action 中。我們用 do 的語法來實(shí)作他。你還記得的話 do 就是把好幾個(gè) I/O action 黏在一起,變成一個(gè)大的 I/O action。
而對(duì)于 Maybe 跟 [] 而言,我們可以把 <*> 想做是從左邊的參數(shù)取出一個(gè)函數(shù),然后套用到右邊的參數(shù)上。至于 IO,這種取出的模擬方式仍然適用,但我們必須多加一個(gè) sequencing 的概念,因?yàn)槲覀兪菑膬蓚€(gè) I/O action 中取值,也是在 sequencing,把他們黏成一個(gè)。我們從第一個(gè) I/O action 中取值,但要取出 I/O action 的結(jié)果,他必須要先被執(zhí)行過。
考慮下面這個(gè)范例:
myAction :: IO String
myAction = do
a <- getLine
b <- getLine
return $ a ++ b
這是一個(gè)提示用戶輸入兩行并產(chǎn)生將兩行輸入串接在一起結(jié)果的一個(gè) I/O action。我們先把兩個(gè) getLine 黏在一起,然后用一個(gè) return,這是因?yàn)槲覀兿胍@個(gè)黏成的 I/O action 包含 a ++ b 的結(jié)果。我們也可以用 applicative style 的方式來描述:
myAction :: IO String
myAction = (++) <$> getLine <*> getLine
我們先前的作法是將兩個(gè) I/O action 的結(jié)果喂給函數(shù)。還記得 getLine 的型別是 getLine :: IO String。當(dāng)我們對(duì) applicative functor 使用 <*> 的時(shí)候,結(jié)果也會(huì)是 applicative functor。
如果我們?cè)偈褂煤凶拥哪M,我們可以把 getLine 想做是一個(gè)去真實(shí)世界中拿取字串的盒子。而 (++) <$> getLine <*> getLine 會(huì)創(chuàng)造一個(gè)比較大的盒子,這個(gè)大盒子會(huì)派兩個(gè)盒子去終端拿取字串,并把結(jié)果串接起來放進(jìn)自己的盒子中。
(++) <$> getLine <*> getLine 的型別是 IO String,他代表這個(gè)表達(dá)式式一個(gè)再普通不過的 I/O action,他里面也裝著某種值。這也是為什么我們可以這樣寫:
main = do
a <- (++) <$> getLine <*> getLine
putStrLn $ "The two lines concatenated turn out to be: " ++ a
如果你發(fā)現(xiàn)你是在做 binding I/O action 的動(dòng)作,而且在 binding 之后還調(diào)用一些函數(shù),最后用 return 來將結(jié)果包起來。 那你可以考慮使用 applicative style,這樣可以更簡潔。
另一個(gè) Applicative 的 instance 是 (->) r。雖然他們通常是用在 code golf 的情況,但他們還是十分有趣的例子。所以我們還是來看一下他們是怎么被實(shí)作的。
如果你忘記 ``(->) r`` 的意思,回去翻翻前一章節(jié)我們介紹 ``(->) r`` 作為一個(gè) functor 的范例。
instance Applicative ((->) r) where
pure x = (\_ -> x)
f <*> g = \x -> f x (g x)
當(dāng)我們用 pure 將一個(gè)值包成 applicative functor 的時(shí)候,他產(chǎn)生的結(jié)果永遠(yuǎn)都會(huì)是那個(gè)值。也就是最小的 context。那也是為什么對(duì)于 function 的 pure 實(shí)作來講,他就是接受一個(gè)值,然后造一個(gè)函數(shù)永遠(yuǎn)回傳那個(gè)值,不管他被喂了什么參數(shù)。如果你限定 pure 的型別至 (->) r 上,他就會(huì)是 pure :: a -> (r -> a)。
ghci> (pure 3) "blah"
3
由于 currying 的關(guān)系,函數(shù)套用是 left-associative,所以我們忽略掉括弧。
ghci> pure 3 "blah"
3
而 <*> 的實(shí)作是比較不容易了解的,我們最好看一下怎么用 applicative style 的方式來使用作為 applicative functor 的 function。
ghci> :t (+) <$> (+3) <*> (*100)
(+) <$> (+3) <*> (*100) :: (Num a) => a -> a
ghci> (+) <$> (+3) <*> (*100) $ 5
508
將兩個(gè) applicative functor 喂給 <*> 可以產(chǎn)生一個(gè)新的 applicative functor,所以如果我們丟給他兩個(gè)函數(shù),我們能得到一個(gè)新的函數(shù)。所以是怎么一回事呢?當(dāng)我們做 (+) <$> (+3) <*> (*100),我們是在實(shí)作一個(gè)函數(shù),他會(huì)將 (+3) 跟 (*100) 的結(jié)果再套用 +。要看一個(gè)實(shí)際的范例的話,可以看一下 (+) <$> (+3) <*> (*100) $ 5 首先 5 被丟給 (+3) 跟 (*100),產(chǎn)生 8 跟 500。然后 + 被套用到 8 跟 500,得到 508。
ghci> (\x y z -> [x,y,z]) <$> (+3) <*> (*2) <*> (/2) $ 5
[8.0,10.0,2.5]
這邊也一樣。我們創(chuàng)建了一個(gè)函數(shù),他會(huì)調(diào)用 \x y z -> [x,y,z],而丟的參數(shù)是 (+3), (*2) 跟 (/2)。5 被丟給以上三個(gè)函數(shù),然后他們結(jié)果又接到 \x y z -> [x, y, z]。
你可以將函數(shù)想做是裝著最終結(jié)果的盒子,所以 k <$> f <*> g 會(huì)制造一個(gè)函數(shù),他會(huì)將 f 跟 g 的結(jié)果丟給 k。當(dāng)我們做 (+) <$> Just 3 <*> Just 5,我們是用 + 套用在一些可能有或可能沒有的值上,所以結(jié)果也會(huì)是可能有或沒有。當(dāng)我們做 (+) <$> (+10) <*> (+5),我們是將 + 套用在 (+10) 跟 (+5) 的結(jié)果上,而結(jié)果也會(huì)是一個(gè)函數(shù),當(dāng)被喂給一個(gè)參數(shù)的時(shí)候會(huì)產(chǎn)生結(jié)果。
我們通常不會(huì)將函數(shù)當(dāng)作 applicative 用,不過仍然值得當(dāng)作練習(xí)。對(duì)于 (->) r 怎么定義成 Applicative 的并不是真的那么重要,所以如果你不是很懂的話也沒關(guān)系。這只是讓你獲得一些操作上的直覺罷了。
一個(gè)我們之前還沒碰過的 Applicative 的 instance 是 ZipList,他是包含在 Control.Applicative 中。
對(duì)于 list 要作為一個(gè) applicative functor 可以有多種方式。我們已經(jīng)介紹過其中一種。如果套用 <*>,左邊是許多函數(shù),而右邊是許多值,那結(jié)果會(huì)是函數(shù)套用到值的所有組合。如果我們做 [(+3),(*2)] <*> [1,2]。那 (+3) 會(huì)先套用至 1 跟 2。接著 (*2) 套用至 1 跟 2。而得到 [4,5,2,4]。
然而 [(+3),(*2)] <*> [1,2] 也可以這樣運(yùn)作:把左邊第一個(gè)函數(shù)套用至右邊第一個(gè)值,接著左邊第二個(gè)函數(shù)套用右邊第二個(gè)值,以此類推。這樣得到的會(huì)是 [4,4]?;蚴?nbsp;[1 + 3, 2 * 2]。
由于一個(gè)型別不能對(duì)同一個(gè) typeclass 定義兩個(gè) instance,所以才會(huì)定義了 ZipList a,他只有一個(gè)構(gòu)造子 ZipList,他只包含一個(gè)字段,他的型別是 list。
instance Applicative ZipList where
pure x = ZipList (repeat x)
ZipList fs <*> ZipList xs = ZipList (zipWith (\f x -> f x) fs xs)
<*> 做的就是我們之前說的。他將第一個(gè)函數(shù)套用至第一個(gè)值,第二個(gè)函數(shù)套用第二個(gè)值。這也是 zipWith (\f x -> f x) fs xs 做的事。由于 zipWith 的特性,所以結(jié)果會(huì)跟 list 中比較短的那個(gè)一樣長。
pure 也值得我們討論一下。他接受一個(gè)值,把他重復(fù)地放進(jìn)一個(gè) list 中。pure "haha" 就會(huì)是 ZipList (["haha","haha","haha"...。這可能會(huì)造成些混淆,畢竟我們說過 pure 是把一個(gè)值放進(jìn)一個(gè)最小的 context 中。而你會(huì)想說無限長的 list 不可能會(huì)是一個(gè)最小的 context。但對(duì)于 zip list 來說這是很合理的,因?yàn)樗仨氃?list 的每個(gè)位置都有值。這也遵守了 pure f <*> xs 必須要等價(jià)于 fmap f xs 的特性。如果 pure 3 只是回傳 ZipList [3],那 pure (*2) <*> ZipList [1,5,10] 就只會(huì)算出 ZipList [2],因?yàn)閮蓚€(gè) zip list 算出結(jié)果的長度會(huì)是比較短的那個(gè)的長度。如果我們 zip 一個(gè)有限長的 list 以及一個(gè)無限長的 list,那結(jié)果的長會(huì)是有限長的 list 的長度。
那 zip list 是怎么用 applicative style 操作的呢?我們來看看,ZipList a 型別并沒有定義成 Show 的 instance,所以我們必須用 getZipList 函數(shù)來從 zip list 取出一個(gè)普通的 list。
ghci> getZipList $ (+) <$> ZipList [1,2,3] <*> ZipList [100,100,100]
[101,102,103]
ghci> getZipList $ (+) <$> ZipList [1,2,3] <*> ZipList [100,100..]
[101,102,103]
ghci> getZipList $ max <$> ZipList [1,2,3,4,5,3] <*> ZipList [5,3,1,2]
[5,3,3,4]
ghci> getZipList $ (,,) <$> ZipList "dog" <*> ZipList "cat" <*> ZipList "rat"
[('d','c','r'),('o','a','a'),('g','t','t')]
``(,,)`` 函數(shù)跟 ``\x y z -> (x,y,z)`` 是等價(jià)的,而 ``(,)`` 跟 ``\x y -> (x,y)`` 是等價(jià)的。
除了 zipWith,標(biāo)準(zhǔn)函式庫中也有 zipWith3, zipWith4 之類的函數(shù),最多支持到 7。zipWith 接受一個(gè)接受兩個(gè)參數(shù)的函數(shù),并把兩個(gè) list zip 起來。zipWith3 則接受一個(gè)接受三個(gè)參數(shù)的函數(shù),然后把三個(gè) list zip 起來。以此類推。用 applicative style 的方式來操作 zip list 的話,我們就不需要對(duì)每個(gè)數(shù)量的 list 都定義一個(gè)獨(dú)立的 zip 函數(shù)來 zip 他們。我們只需要用 applicative style 的方式來把任意數(shù)量的 list zip 起來就可以了。
Control.Applicative 定義了一個(gè)函數(shù)叫做 liftA2,他的型別是 liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c。他定義如下:
liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c
liftA2 f a b = f <$> a <*> b
并沒有太難理解的東西,他不過就是對(duì)兩個(gè) applicatives 套用函數(shù)而已,而不用我們剛剛熟悉的 applicative style。我們提及他的理由只是要展示為什么 applicative functors 比起一般的普通 functor 要強(qiáng)。如果只是普通的 functor 的話,我們只能將一個(gè)函數(shù) map over 這個(gè) functor。但有了 applicative functor,我們可以對(duì)好多個(gè) functor 套用一個(gè)函數(shù)??纯催@個(gè)函數(shù)的型別,他會(huì)是 (a -> b -> c) -> (f a -> f b -> f c)。當(dāng)我們從這樣的角度來看他的話,我們可以說 liftA2 接受一個(gè)普通的二元函數(shù),并將他升級(jí)成一個(gè)函數(shù)可以運(yùn)作在兩個(gè) functor 之上。
另外一個(gè)有趣的概念是,我們可以接受兩個(gè) applicative functor 并把他們結(jié)合成一個(gè) applicative functor,這個(gè)新的將這兩個(gè) applicative functor 裝在 list 中。舉例來說,我們現(xiàn)在有 Just 3 跟 Just 4。我們假設(shè)后者是一個(gè)只包含單元素的 list。
ghci> fmap (\x -> [x]) (Just 4)
Just [4]
所以假設(shè)我們有 Just 3 跟 Just [4]。我們有怎么得到 Just [3,4] 呢?很簡單。
ghci> liftA2 (:) (Just 3) (Just [4])
Just [3,4]
ghci> (:) <$> Just 3 <*> Just [4]
Just [3,4]
還記得 : 是一個(gè)函數(shù),他接受一個(gè)元素跟一個(gè) list,并回傳一個(gè)新的 list,其中那個(gè)元素已經(jīng)接在前面?,F(xiàn)在我們有了 Just [3,4],我們能夠?qū)⑺?nbsp;Just 2 綁在一起變成 Just [2,3,4] 嗎?當(dāng)然可以。我們可以將任意數(shù)量的 applicative 綁在一起變成一個(gè) applicative,里面包含一個(gè)裝有結(jié)果的 list。我們?cè)囍鴮?shí)作一個(gè)函數(shù),他接受一串裝有 applicative 的 list,然后回傳一個(gè) applicative 里面有一個(gè)裝有結(jié)果的 list。我們稱呼他為 sequenceA。
sequenceA :: (Applicative f) => [f a] -> f [a]
sequenceA [] = pure []
sequenceA (x:xs) = (:) <$> x <*> sequenceA xs
居然用到了遞歸!首先我們來看一下他的型別。他將一串 applicative 的 list 轉(zhuǎn)換成一個(gè) applicative 裝有一個(gè) list。從這個(gè)信息我們可以推測出邊界條件。如果我們要將一個(gè)空的 list 變成一個(gè)裝有 list 的 applicative。我們只要把這個(gè)空的 list 放進(jìn)一個(gè)缺省的 context?,F(xiàn)在來看一下我們?cè)趺从眠f歸的。如果們有一個(gè)可以分成頭跟尾的 list(x 是一個(gè) applicative 而 xs 是一串 applicatve),我們可以對(duì)尾巴調(diào)用 sequenceA,便會(huì)得到一個(gè)裝有 list 的 applicative。然后我們只要將在 x 中的值把他接到裝有 list 的 applicative 前面就可以了。
所以如果我們做 sequenceA [Just 1, Just 2],也就是 (:) <$> Just 1 <*> sequenceA [Just 2]。那會(huì)等價(jià)于 (:) <$> Just 1 <*> ((:) <$> Just 2 <*> sequenceA [])。我們知道 sequenceA [] 算出來會(huì)是 Just [],所以運(yùn)算式就變成 (:) <$> Just 1 <*> ((:) <$> Just 2 <*> Just []),也就是 (:) <$> Just 1 <*> Just [2],算出來就是 Just [1,2]。
另一種實(shí)作 sequenceA 的方式是用 fold。要記得幾乎任何需要走遍整個(gè) list 并 accumulate 成一個(gè)結(jié)果的都可以用 fold 來實(shí)作。
sequenceA :: (Applicative f) => [f a] -> f [a]
sequenceA = foldr (liftA2 (:)) (pure [])
我們從右往左走,并且起始的 accumulator 是用 pure []。我們是用 liftA2 (:) 來結(jié)合 accumulator 跟 list 中最后的元素,而得到一個(gè) applicative,里面裝有一個(gè)單一元素的一個(gè) list。然后我們?cè)儆?nbsp;liftA2 (:) 來結(jié)合 accumulator 跟最后一個(gè)元素,直到我們只剩下 accumulator 為止,而得到一個(gè) applicative,里面裝有所有結(jié)果。
我們來試試看套用在不同 applicative 上。
ghci> sequenceA [Just 3, Just 2, Just 1]
Just [3,2,1]
ghci> sequenceA [Just 3, Nothing, Just 1]
Nothing
ghci> sequenceA [(+3),(+2),(+1)] 3
[6,5,4]
ghci> sequenceA [[1,2,3],[4,5,6]]
[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]
ghci> sequenceA [[1,2,3],[4,5,6],[3,4,4],[]]
[]
很酷吧。當(dāng)我們套用在 Maybe 上時(shí),sequenceA 創(chuàng)造一個(gè)新的 Maybe,他包含了一個(gè) list 裝有所有結(jié)果。如果其中一個(gè)值是 Nothing,那整個(gè)結(jié)果就會(huì)是 Nothing。如果你有一串 Maybe 型別的值,但你只在乎當(dāng)結(jié)果不包含任何 Nothing 的情況,這樣的特性就很方便。
當(dāng)套用在函數(shù)時(shí),sequenceA 接受裝有一堆函數(shù)的 list,并回傳一個(gè)回傳 list 的函數(shù)。在我們的范例中,我們寫了一個(gè)函數(shù),他只接受一個(gè)數(shù)值作為參數(shù),他會(huì)把他套用至 list 中的每一個(gè)函數(shù),并回傳一個(gè)包含結(jié)果的 list。sequenceA [(+3),(+2),(+1)] 3 會(huì)將 3 喂給 (+3), (+2) 跟 (+1),然后將所有結(jié)果裝在一個(gè) list 中。
而 (+) <$> (+3) <*> (*2) 會(huì)創(chuàng)見一個(gè)接受單一參數(shù)的一函數(shù),將他同時(shí)喂給 (+3) 跟 (*2),然后調(diào)用 + 來將兩者加起來。同樣的道理,sequenceA [(+3),(*2)] 是制造一個(gè)接受單一參數(shù)的函數(shù),他會(huì)將他喂給所有包含在 list 中的函數(shù)。但他最后不是調(diào)用 +,而是調(diào)用 : 跟 pure [] 來把結(jié)果接成一個(gè) list,得到最后的結(jié)果。
當(dāng)我們有一串函數(shù),我們想要將相同的輸入都喂給他們并查看結(jié)果的時(shí)候,sequenceA 非常好用。例如說,我們手上有一個(gè)數(shù)值,但不知道他是否滿足一串 predicate。一種實(shí)作的方式是像這樣:
ghci> map (\f -> f 7) [(>4),(<10),odd]
[True,True,True]
ghci> and $ map (\f -> f 7) [(>4),(<10),odd]
True
記住 and 接受一串布林值,并只有在全部都是 True 的時(shí)候才回傳 True。 另一種實(shí)作方式是用 sequenceA:
ghci> sequenceA [(>4),(<10),odd] 7
[True,True,True]
ghci> and $ sequenceA [(>4),(<10),odd] 7
True
sequenceA [(>4),(<10),odd] 接受一個(gè)函數(shù),他接受一個(gè)數(shù)值并將他喂給所有的 predicate,包含 [(>4),(<10),odd]。然后回傳一串布林值。他將一個(gè)型別為 (Num a) => [a -> Bool] 的 list 變成一個(gè)型別為 (Num a) => a -> [Bool] 的函數(shù),很酷吧。
由于 list 要求里面元素的型別要一致,所以包含在 list 中的所有函數(shù)都是同樣型別。你不能創(chuàng)造一個(gè)像是 [ord, (+3)] 這樣的 list,因?yàn)?nbsp;ord 接受一個(gè)字符并回傳一個(gè)數(shù)值,然而 (+3) 接受一個(gè)數(shù)值并回傳一個(gè)數(shù)值。
當(dāng)跟 [] 一起使用的時(shí)候,sequenceA 接受一串 list,并回傳另一串 list。他實(shí)際上是創(chuàng)建一個(gè)包含所有可能組合的 list。為了方便說明,我們比較一下使用 sequenceA 跟 list comprehension 的差異:
ghci> sequenceA [[1,2,3],[4,5,6]]
[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]
ghci> [[x,y] | x <- [1,2,3], y <- [4,5,6]]
[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]
ghci> sequenceA [[1,2],[3,4]]
[[1,3],[1,4],[2,3],[2,4]]
ghci> [[x,y] | x <- [1,2], y <- [3,4]]
[[1,3],[1,4],[2,3],[2,4]]
ghci> sequenceA [[1,2],[3,4],[5,6]]
[[1,3,5],[1,3,6],[1,4,5],[1,4,6],[2,3,5],[2,3,6],[2,4,5],[2,4,6]]
ghci> [[x,y,z] | x <- [1,2], y <- [3,4], z <- [5,6]]
[[1,3,5],[1,3,6],[1,4,5],[1,4,6],[2,3,5],[2,3,6],[2,4,5],[2,4,6]]
這可能有點(diǎn)難以理解,但如果你多做點(diǎn)嘗試,你會(huì)比較能看出來些眉目。假設(shè)我們?cè)谧?nbsp;sequenceA [[1,2],[3,4]]。要知道這是怎么回事,我們首先用 sequenceA 的定義 sequenceA (x:xs) = (:) <$> x <*> sequenceA xs 還有邊界條件 sequenceA [] = pure [] 來看看。你不需要實(shí)際計(jì)算,但他可以幫助你理解 sequenceA 是怎么運(yùn)作在一串 list 上,畢竟這有點(diǎn)復(fù)雜。
# 我們從 ``sequenceA [[1,2],[3,4]]`` 開始
# 那可以被計(jì)算成 ``(:) <$> [1,2] <*> sequenceA [[3,4]]``
# 計(jì)算內(nèi)層的 ``sequenceA``,會(huì)得到 ``(:) <$> [1,2] <*> ((:) <$> [3,4] <*> sequenceA [])``
# 我們碰到了邊界條件,所以會(huì)是 ``(:) <$> [1,2] <*> ((:) <$> [3,4] <*> [[]])``
# 現(xiàn)在我們計(jì)算 ``(:) <$> [3,4] <*> [[]] `` 的部份,我們會(huì)對(duì)左邊 list 中的每一個(gè)值 (也就是 ``3`` 跟 ``4``) 跟右邊的每一個(gè)值 (只有 ``[]``)套用 ``:``,而得到 ``[3:[], 4:[]]``,也就是 ``[[3],[4]]``。所以我們有 ``(:) <$> [1,2] <*> [[3],[4]]``
# 而對(duì)于左邊的每一個(gè)值(``1`` 跟 ``2``)以及右邊可能的值(``[3]`` 跟 ``[4]``)我們套用 ``:`` 而得到 ``[1:[3], 1:[4], 2:[3], 2:[4]]``,他等于 ``[[1,3],[1,4],[2,3],[2,4]]``
計(jì)算 (+) <$> [1,2] <*> [4,5,6] 會(huì)得到一個(gè) non-deterministic 的結(jié)果 x + y,其中 x 代表 [1,2] 中的每一個(gè)值,而 y 代表 [4,5,6] 中的每一個(gè)值。我們用 list 來表示每一種可能的情形。同樣的,當(dāng)我們?cè)谧?nbsp;sequence [[1,2],[3,4],[5,6],[7,8]],他的結(jié)果會(huì)是 non-deterministic 的 [x,y,z,w],其中 x 代表 [1,2] 中的每一個(gè)值,而 y 代表 [3,4] 中的每一個(gè)值。以此類推。我們用 list 代表 non-deterministic 的計(jì)算,每一個(gè)元素都是一個(gè)可能的情形。這也是為什么會(huì)用到 list of list。
當(dāng)使用在 I/O action 上的時(shí)候,sequenceA 跟 sequence 是等價(jià)的。他接受一串 I/O action 并回傳一個(gè) I/O action,這個(gè) I/O action 會(huì)計(jì)算 list 中的每一個(gè) I/O action,并把結(jié)果放在一個(gè) list 中。要將型別為 [IO a] 的值轉(zhuǎn)換成 IO [a] 的值,也就是會(huì)產(chǎn)生一串 list 的一個(gè) I/O action,那這些 I/O action 必須要一個(gè)一個(gè)地被計(jì)算,畢竟對(duì)于這些 I/O action 你沒辦法不計(jì)算就得到結(jié)果。
ghci> sequenceA [getLine, getLine, getLine]
heyh
ho
woo
["heyh","ho","woo"]
就像普通的函數(shù)一樣,applicative functors 也遵循一些定律。其中最重要的一個(gè)是我們之前提過的 pure f <*> x = fmap f x。你可以證明一些我們之前介紹過的 applicative functor 遵守這個(gè)定律當(dāng)作練習(xí)。其他的 functors law 有:
# ``pure id <*> v = v``
# ``pure (.) <*> u <*> v <*> w = u <*> (v <*> w)``
# ``pure f <*> pure x = pure (f x)``
# ``u <*> pure y = pure ($ y) <*> u``
我們不會(huì)一項(xiàng)一項(xiàng)地細(xì)看,那樣會(huì)花費(fèi)很大的篇幅而且對(duì)讀者來說很無聊,但如果你有興趣,你可以針對(duì)某些 instance 看看他們會(huì)不會(huì)遵守。
結(jié)論就是 applicative functor 不只是有趣而且實(shí)用, 他允許我們結(jié)合不同種類的計(jì)算,像是 I/O 計(jì)算,non-deterministic 的計(jì)算,有可能失敗的計(jì)算等等。而使用 <$> 跟 <*> 我們可以將普通的函數(shù)來運(yùn)作在任意數(shù)量的 applicative functors 上。
到目前為止,我們已經(jīng)看過了如何用 data 關(guān)鍵字定義自己的 algebraic data type。我們也學(xué)習(xí)到了如何用 type 來定義 type synonyms。在這個(gè)章節(jié)中,我們會(huì)看一下如何使用 newtype 來從一個(gè)現(xiàn)有的型別中定義出新的型別,并說明我們?yōu)槭裁磿?huì)想要那么做。
在之前的章節(jié)中,我們了解到其實(shí) list 有很多種方式可以被視為一種 applicative functor。一中方式是定義 <*> 將左邊的每一個(gè)值跟右邊的每一個(gè)值組合,而得到各種組合的結(jié)果。
ghci> [(+1),(*100),(*5)] <*> [1,2,3]
[2,3,4,100,200,300,5,10,15]
第二種方式是將 <*> 定義成將左邊的第一個(gè)函數(shù)套用至右邊的第一個(gè)值,然后將左邊第二個(gè)函數(shù)套用至右邊第二個(gè)值。以此類推。最終,這表現(xiàn)得有點(diǎn)像將兩個(gè) list 用一個(gè)拉鏈拉起來一樣。但由于 list 已經(jīng)被定義成 Applicaitive 的 instance 了,所以我們要怎么要讓 list 可以被定義成第二種方式呢?如果你還記得我們說過我們是有很好的理由定義了 ZipList a,其中他里面只包含一個(gè)值構(gòu)造子跟只包含一個(gè)字段。其實(shí)他的理由就是要讓 ZipList 定義成用拉鏈的方式來表現(xiàn) applicative 行為。我們只不過用 ZipList 這個(gè)構(gòu)造子將他包起來,然后用 getZipList 來解開來。
ghci> getZipList $ ZipList [(+1),(*100),(*5)] <*> ZipList [1,2,3]
[2,200,15]
所以這跟 newtype 這個(gè)關(guān)鍵字有什么關(guān)系呢?想想看我們是怎么宣告我們的 ZipList a 的,一種方式是像這樣:
data ZipList a = ZipList [a]
也就是一個(gè)只有一個(gè)值構(gòu)造子的型別而且那個(gè)構(gòu)造子里面只有一個(gè)字段。我們也可以用 record syntax 來定義一個(gè)解開的函數(shù):
data ZipList a = ZipList { getZipList :: [a] }
這樣聽起來不錯(cuò)。這樣我們就有兩種方式來讓一個(gè)型別來表現(xiàn)一個(gè) typeclass,我們可以用 data 關(guān)鍵字來把一個(gè)型別包在另一個(gè)里面,然后再將他定義成第二種表現(xiàn)方式。
而在 Haskell 中 newtype 正是為了這種情形,我們想將一個(gè)型別包在另一個(gè)型別中。在實(shí)際的函式庫中 ZipList a 是這樣定義了:
newtype ZipList a = ZipList { getZipList :: [a] }
這邊我們不用 data 關(guān)鍵字反而是用 newtype 關(guān)鍵字。這是為什么呢?第一個(gè)理由是 newtype 比較快速。如果你用 data 關(guān)鍵字來包一個(gè)型別的話,在你執(zhí)行的時(shí)候會(huì)有一些包起來跟解開來的成本。但如果你用 newtype 的話,Haskell 會(huì)知道你只是要將一個(gè)現(xiàn)有的型別包成一個(gè)新的型別,你想要內(nèi)部運(yùn)作完全一樣但只是要一個(gè)全新的型別而已。有了這個(gè)概念,Haskell 可以將包裹跟解開來的成本都去除掉。
那為什么我們不是一直使用 newtype 呢?當(dāng)你用 newtype 來制作一個(gè)新的型別時(shí),你只能定義單一一個(gè)值構(gòu)造子,而且那個(gè)構(gòu)造子只能有一個(gè)字段。但使用 data 的話,你可以讓那個(gè)型別有好幾個(gè)值構(gòu)造子,并且每個(gè)構(gòu)造子可以有零個(gè)或多個(gè)字段。
data Profession = Fighter | Archer | Accountant
data Race = Human | Elf | Orc | Goblin
data PlayerCharacter = PlayerCharacter Race Profession
當(dāng)使用 newtype的時(shí)候,你是被限制只能用一個(gè)值構(gòu)造子跟單一字段。
對(duì)于 newtype 我們也能使用 deriving 關(guān)鍵字。我們可以 derive 像是 Eq, Ord, Enum, Bounded, Show 跟 Read 的 instance。如果我們想要對(duì)新的型別做 derive,那原本的型別必須已經(jīng)在那個(gè) typeclass 中。這樣很合理,畢竟 newtype 就是要將現(xiàn)有的型別包起來。如果我們按照下面的方式定義的話,我們就能對(duì)我們的型別做印出以及比較相等性的操作:
newtype CharList = CharList { getCharList :: [Char] } deriving (Eq, Show)
我們來跑跑看:
ghci> CharList "this will be shown!"
CharList {getCharList = "this will be shown!"}
ghci> CharList "benny" == CharList "benny"
True
ghci> CharList "benny" == CharList "oisters"
False
對(duì)于這個(gè) newtype,他的值構(gòu)造子有下列型別:
CharList :: [Char] -> CharList
他接受一個(gè) [Char] 的值,例如 "my sharona" 并回傳一個(gè) CharList 的值。從上面我們使用 CharList 的值構(gòu)造子的范例中,我們可以看到的確是這樣。相反地,getCharList 具有下列的型別。
getCharList :: CharList -> [Char]
他接受一個(gè) CharList 的值并將他轉(zhuǎn)成 [Char]。你可以將這個(gè)想成包裝跟解開的動(dòng)作,但你也可以將他想成從一個(gè)型別轉(zhuǎn)成另一個(gè)型別。
有好幾次我們想要讓我們的型別屬于某個(gè) typeclass,但型別變量并沒有符合我們想要的。要把 Maybe 定義成 Functor 的 instance 很容易,因?yàn)?nbsp;Functor 這個(gè) typeclass 被定義如下:
class Functor f where
fmap :: (a -> b) -> f a -> f b
我們先定義如下:
instance Functor Maybe where
然后我們實(shí)作 fmap。當(dāng)所有的型別變量被填上時(shí),由于 Maybe 取代了 Functor 中 f 的位置,所以如果我們看看 fmap 運(yùn)作在 Maybe 上時(shí)是什么樣,他會(huì)像這樣:
fmap :: (a -> b) -> Maybe a -> Maybe b
看起來不錯(cuò)吧?現(xiàn)在我們想要 tuple 成為 Functor 的一個(gè) instance,所以當(dāng)我們用 fmap 來 map over 一個(gè) tuple 時(shí),他會(huì)先套用到 tuple 中的第一個(gè)元素。這樣當(dāng)我們做 fmap (+3) (1,1) 會(huì)得到 (4,1)。不過要定義出這樣的 instance 有些困難。對(duì)于 Maybe,我們只要寫 instance Functor Maybe where,這是因?yàn)閷?duì)于只吃一個(gè)參數(shù)的型別構(gòu)造子我們很容易定義成 Functor 的 instance。但對(duì)于 (a,b) 這樣的就沒辦法。要繞過這樣的困境,我們可以用 newtype 來重新定義我們的 tuple,這樣第二個(gè)型別參數(shù)就代表了 tuple 中的第一個(gè)元素部份。
newtype Pair b a = Pair { getPair :: (a,b) }
現(xiàn)在我們可以將他定義成 Functor 的 instance,所以函數(shù)被 map over tuple 中的第一個(gè)部份。
instance Functor (Pair c) where
fmap f (Pair (x,y)) = Pair (f x, y)
正如你看到的,我們可以對(duì) newtype 定義的型別做模式匹配。我們用模式匹配來拿到底層的 tuple,然后我們將 f 來套用至 tuple 的第一個(gè)部份,然后我們用 Pair 這個(gè)值構(gòu)造子來將 tuple 轉(zhuǎn)換成 Pair b a。如果我們問 fmap 的型別究竟是什么,他會(huì)是:
fmap :: (a -> b) -> Pair c a -> Pair c b
我們說過 instance Functor (Pair c) where 跟 Pair c 取代了 Functor 中 f 的位置:
class Functor f where
fmap :: (a -> b) -> f a -> f b
如果我們將一個(gè) tuple 轉(zhuǎn)換成 Pair b a,我們可以用 fmap 來 map over 第一個(gè)部份。
ghci> getPair $ fmap (*100) (Pair (2,3))
(200,3)
ghci> getPair $ fmap reverse (Pair ("london calling", 3))
("gnillac nodnol",3)
我們提到 newtype 一般來講比 data 來得有效率。newtype 能做的唯一一件事就是將現(xiàn)有的型別包成新的型別。這樣 Haskell 在內(nèi)部就能將新的型別的值用舊的方式來操作。只是要記住他們還是不同的型別。這代表 newtype 并不只是有效率,他也具備 lazy 的特性。我們來說明一下這是什么意思。
就像我們之前說得,Haskell 缺省是具備 lazy 的特性,這代表只有當(dāng)我們要將函數(shù)的結(jié)果印出來的時(shí)候計(jì)算才會(huì)發(fā)生?;蛘哒f,只有當(dāng)我們真的需要結(jié)果的時(shí)候計(jì)算才會(huì)發(fā)生。在 Haskell 中 undefined 代表會(huì)造成錯(cuò)誤的計(jì)算。如果我們?cè)囍?jì)算他,也就是將他印到終端中,Haskell 會(huì)丟出錯(cuò)誤。
ghci> undefined
*** Exception: Prelude.undefined
然而,如果我們做一個(gè) list,其中包含一些 undefined 的值,但卻要求一個(gè)不是 undefined 的 head,那一切都會(huì)順利地被計(jì)算,因?yàn)?Haskell 并不需要 list 中其他元素來得到結(jié)果。我們僅僅需要看到第一個(gè)元素而已。
ghci> head [3,4,5,undefined,2,undefined]
3
現(xiàn)在們考慮下面的型別:
data CoolBool = CoolBool { getCoolBool :: Bool }
這是一個(gè)用 data 關(guān)鍵字定義的 algebraic data type。他有一個(gè)值建構(gòu)子并只有一個(gè)型別為 Bool 的字段。我們寫一個(gè)函數(shù)來對(duì) CoolBool 做模式匹配,并回傳一個(gè) "hello" 的值。他并不會(huì)管 CoolBool 中裝的究竟是 True 或 False。
helloMe :: CoolBool -> String
helloMe (CoolBool _) = "hello"
這次我們不喂給這個(gè)函數(shù)一個(gè)普通的 CoolBool,而是丟給他一個(gè) undefined。
ghci> helloMe undefined
"*** Exception: Prelude.undefined "
結(jié)果收到了一個(gè) Exception。是什么造成這個(gè) Exception 的呢?用 data 定義的型別可以有好幾個(gè)值構(gòu)造子(盡管 CoolBool 只有一個(gè))所以當(dāng)我們要看看喂給函數(shù)的值是否是 (CoolBool _) 的形式,Haskell 會(huì)需要做一些基本的計(jì)算來看看是哪個(gè)值構(gòu)造子被用到。但當(dāng)我們計(jì)算 undefined 的時(shí)候,就算是一點(diǎn)也會(huì)丟出 Exception。
我們不用 data 來定義 CoolBool 而用 newtype:
newtype CoolBool = CoolBool { getCoolBool :: Bool }
我們不用修改 helloMe 函數(shù),因?yàn)閷?duì)于模式匹配使用 newtype 或 data 都是一樣。我們?cè)賮韺?nbsp;undefined 喂給 helloMe。
ghci> helloMe undefined
"hello"
居然正常運(yùn)作!為什么呢?正如我們說過得,當(dāng)我們使用 newtype 的時(shí)候,Haskell 內(nèi)部可以將新的型別用舊的型別來表示。他不必加入另一層 box 來包住舊有的型別。他只要注意他是不同的型別就好了。而且 Haskell 會(huì)知道 newtype 定義的型別一定只會(huì)有一個(gè)構(gòu)造子,他不必計(jì)算喂給函數(shù)的值就能確定他是 (CoolBool _) 的形式,因?yàn)?nbsp;newtype 只有一個(gè)可能的值跟單一字段!
這樣行為的差異可能沒什么關(guān)系,但實(shí)際上他非常重要。因?yàn)樗屛覀冋J(rèn)知到盡管從撰寫程序的觀點(diǎn)來看沒什么差異,但他們的確是兩種不同的機(jī)制。盡管 data 可以讓你從無到有定義型別,newtype 是從一個(gè)現(xiàn)有的型別做出來的。對(duì) newtype 做模式匹配并不是像從盒子中取出東西,他比較像是將一個(gè)型別轉(zhuǎn)換成另一個(gè)型別。
到目前為止,你也許對(duì)于 type,data 跟 newtype 之間的差異還不是很了解,讓我們快速復(fù)習(xí)一遍。
type 關(guān)鍵字是讓我們定義 type synonyms。他代表我們只是要給一個(gè)現(xiàn)有的型別另一個(gè)名字,假設(shè)我們這樣做:
type IntList = [Int]
這樣做可以允許我們用 IntList 的名稱來指稱 [Int]。我們可以交換地使用他們。但我們并不會(huì)因此有一個(gè) IntList 的值構(gòu)造子。因?yàn)?nbsp;[Int] 跟 IntList 只是兩種指稱同一個(gè)型別的方式。我們?cè)谥阜Q的時(shí)候用哪一個(gè)并無所謂。
ghci> ([1,2,3] :: IntList) ++ ([1,2,3] :: [Int])
[1,2,3,1,2,3]
當(dāng)我們想要讓 type signature 更清楚一些,給予我們更了解函數(shù)的 context 的時(shí)候,我們會(huì)定義 type synonyms。舉例來說,當(dāng)我們用一個(gè)型別為 [(String,String)] 的 association list 來代表一個(gè)電話簿的時(shí)候,我們可以定義一個(gè) PhoneBook 的 type synonym,這樣 type signature 會(huì)比較容易讀。
newtype 關(guān)鍵字將現(xiàn)有的型別包成一個(gè)新的型別,大部分是為了要讓他們可以是特定 typeclass 的 instance 而這樣做。當(dāng)我們使用 newtype 來包裹一個(gè)現(xiàn)有的型別時(shí),這個(gè)型別跟原有的型別是分開的。如果我們將下面的型別用 newtype 定義:
newtype CharList = CharList { getCharList :: [Char] }
我們不能用 ++ 來將 CharList 跟 [Char] 接在一起。我們也不能用 ++ 來將兩個(gè) CharList 接在一起,因?yàn)?nbsp;++ 只能套用在 list 上,而 CharList 并不是 list,盡管你會(huì)說他包含一個(gè) list。但我們可以將兩個(gè) CharList 轉(zhuǎn)成 list,將他們 ++ 然后再轉(zhuǎn)回 CharList。
當(dāng)我們?cè)?nbsp;newtype 宣告中使用 record syntax 的時(shí)候,我們會(huì)得到將新的型別轉(zhuǎn)成舊的型別的函數(shù),也就是我們 newtype 的值構(gòu)造子,以及一個(gè)函數(shù)將他的字段取出。新的型別并不會(huì)被自動(dòng)定義成原有型別所屬的 typeclass 的一個(gè) instance,所以我們必須自己來 derive 他們。
實(shí)際上你可以將 newtype 想成是只能定義一個(gè)構(gòu)造子跟一個(gè)字段的 data 宣告。如果你碰到這種情形,可以考慮使用 newtype。
使用 data 關(guān)鍵字是為了定義自己的型別。他們可以在 algebraic data type 中放任意數(shù)量的構(gòu)造子跟字段。可以定義的東西從 list, Maybe 到 tree。
如果你只是希望你的 type signature 看起來比較干凈,你可以只需要 type synonym。如果你想要將現(xiàn)有的型別包起來并定義成一個(gè) type class 的 instance,你可以嘗試使用 newtype。如果你想要定義完全新的型別,那你應(yīng)該使用 data 關(guān)鍵字。
Haskell 中 typeclass 是用來表示一個(gè)型別之間共有的行為,是一種 interface。我們介紹過 Eq,他定義型別是否可以比較相等性,以及 Ord,他表示可以被排序的型別。還介紹了更有趣的像是 Functor 跟 Applicative。
當(dāng)我們定義一個(gè)型別時(shí),我們會(huì)想說他應(yīng)該要支持的行為。也就是表現(xiàn)的行為是什么,并且要讓他屬于哪些 typeclass。如果希望他可以比較相等與否,那我們就應(yīng)該定義他成為 Eq 的一個(gè) instance。如果我們想要看看型別是否是一種 functor,我們可以定義他是 Functor 的一個(gè) instance。以此類推。
考慮 * 是一個(gè)將兩個(gè)數(shù)值相乘的一個(gè)函數(shù)。如果我們將一個(gè)數(shù)值乘上 1,那就會(huì)得到自身的數(shù)值。我們實(shí)際上是做 1 * x 或 x * 1 并沒有差別。結(jié)果永遠(yuǎn)會(huì)是 x。同樣的,++ 是一個(gè)接受兩個(gè)參數(shù)并回傳新的值的一個(gè)函數(shù)。只是他不是相乘而是將兩個(gè) list 接在一起。而類似 *,他也有一個(gè)特定的值,當(dāng)他跟其他值使用 ++ 時(shí)會(huì)得到同樣的值。那個(gè)值就是空的 list []。
ghci> 4 * 1
4
ghci> 1 * 9
9
ghci> [1,2,3] ++ []
[1,2,3]
ghci> [] ++ [0.5, 2.5]
[0.5,2.5]
看起來 * 之于 1 跟 ++ 之于 [] 有類似的性質(zhì):
# 函數(shù)同樣接受兩個(gè)參數(shù)
# 參數(shù)跟回傳值是同樣的型別
# 同樣存在某些值當(dāng)套用二元函數(shù)時(shí)并不會(huì)改變其他值
關(guān)于這兩種操作還有另一個(gè)比較難察覺的性質(zhì)就是,當(dāng)我們對(duì)這個(gè)二元函數(shù)對(duì)三個(gè)以上的值操作并化簡,函數(shù)套用的順序并不會(huì)影響到結(jié)果。不論是 (3 * 4) * 5 或是 3 * (4 * 5),兩種方式都會(huì)得到 60。而 ++ 也是相同的。
ghci> (3 * 2) * (8 * 5)
240
ghci> 3 * (2 * (8 * 5))
240
ghci> "la" ++ ("di" ++ "da")
"ladida"
ghci> ("la" ++ "di") ++ "da"
"ladida"
我們稱呼這樣的性質(zhì)為結(jié)合律(associativity)。* 遵守結(jié)合律,++ 也是。但 - 就不遵守。(5 - 3) - 4 跟 5 - (3 - 4) 得到的結(jié)果是不同的。
注意到這些性質(zhì)并具體地寫下來,就可以得到 monoid。一個(gè) monoid 是你有一個(gè)遵守結(jié)合律的二元函數(shù)還有一個(gè)可以相對(duì)于那個(gè)函數(shù)作為 identity 的值。當(dāng)某個(gè)值相對(duì)于一個(gè)函數(shù)是一個(gè) identity,他表示當(dāng)我們將這個(gè)值丟給函數(shù)時(shí),結(jié)果永遠(yuǎn)會(huì)是另外一邊的那個(gè)值本身。1 是相對(duì)于 * 的 identity,而 [] 是相對(duì)于 ++ 的 identity。在 Haskell 中還有許多其他的 monoid,這也是為什么我們定義了 Monoid 這個(gè) typeclass。他描述了表現(xiàn)成 monoid 的那些型別。我們來看看這個(gè) typeclass 是怎么被定義的:
class Monoid m where
mempty :: m
mappend :: m -> m -> m
mconcat :: [m] -> m
mconcat = foldr mappend mempty
Monoid typeclass 被定義在 import Data.Monoid 中。我們來花些時(shí)間好好了解他。
首先我們看到只有具體型別才能定義成 Monoid 的 instance。由于在 typeclass 定義中的 m 并不接受任何型別參數(shù)。這跟 Functor 以及 Applicative 不同,他們要求他們的 instance 必須是一個(gè)接受單一型別參數(shù)的型別構(gòu)造子。
第一個(gè)函數(shù)是 mempty,由于他不接受任何參數(shù),所以他并不是一個(gè)函數(shù),而是一個(gè) polymorphic 的常數(shù)。有點(diǎn)像是 Bounded 中的 minBound 一樣。mempty 表示一個(gè)特定 monoid 的 identity。
再來我們看到 mappend,你可能已經(jīng)猜到,他是一個(gè)接受兩個(gè)相同型別的值的二元函數(shù),并回傳同樣的型別。不過要注意的是他的名字不太符合他真正的意思,他的名字隱含了我們要將兩個(gè)東西接在一起。盡管在 list 的情況下 ++ 的確將兩個(gè) list 接起來,但 * 則否。他只不過將兩個(gè)數(shù)值做相乘。當(dāng)我們?cè)倏吹狡渌?nbsp;Monoid 的 instance 時(shí),我們會(huì)看到他們大部分都沒有接起來的做,所以不要用接起來的概念來想像 mappend,只要想像他們是接受兩個(gè) monoid 的值并回傳另外一個(gè)就好了。
在 typeclass 定義中的最后一個(gè)函數(shù)是 mconcat。他接受一串 monoid 值,并將他們用 mappend 簡化成單一的值。他有一個(gè)缺省的實(shí)作,就是從 mempty 作為起始值,然后用 mappend 來 fold。由于對(duì)于大部分的 instance 缺省的實(shí)作就沒什么問題,我們不會(huì)想要實(shí)作自己的 mconcat。當(dāng)我們定義一個(gè)型別屬于 Monoid 的時(shí)候,多半實(shí)作 mempty 跟 mappend 就可以了。而 mconcat 就是因?yàn)閷?duì)于一些 instance,有可能有比較有效率的方式來實(shí)作 mconcat。不過大多數(shù)情況都不需要。
在我們繼續(xù)接下去看幾個(gè) Monoid 的例子前,我們來看一下 monoid law。我們提過必須有一個(gè)值作為 identity 以及一個(gè)遵守結(jié)合律的二元函數(shù)當(dāng)作前提。我們是可以定義一個(gè) Monoid 的 instance 卻不遵守這些定律的,但這樣寫出來的 instance 就沒有用了,因?yàn)槲覀冊(cè)谑褂?nbsp;Monoid 的時(shí)候都是依靠這些定律才可以稱作實(shí)質(zhì)上的 monoid。所以我們必須確保他們遵守:
# ``mempty `mappend` x = x``
# ``x `mappend` mempty = x``
# ``(x `mappend` y) `mappend` z = x `mappend` (y `mappend` z)``
前兩個(gè)描述了 mempty 相對(duì)于 mappend 必須要表現(xiàn)成 identity。而第三個(gè)定律說了 mappend 必須要遵守結(jié)合律。也就是說我們做 mappend 順序并不重要。Haskell 不會(huì)自己檢查這些定律是否有被遵守。所以你必須自己小心地檢查他們。
沒錯(cuò),list 是一種 monoid。正如我們先前看到的,++ 跟空的 list [] 共同形成了一個(gè) monoid。他的 instance 很簡單:
instance Monoid [a] where
mempty = []
mappend = (++)
list 是 Monoid typeclass 的一個(gè) instance,這跟他們裝的元素的型別無關(guān)。注意到我們寫 instance Monoid [a] 而非 instance Monoid [],這是因?yàn)?nbsp;Monoid 要求 instance 必須是具體型別。
我們?cè)囍芘芸?,得到我們預(yù)期中的結(jié)果:
ghci> [1,2,3] `mappend` [4,5,6]
[1,2,3,4,5,6]
ghci> ("one" `mappend` "two") `mappend` "tree"
"onetwotree"
ghci> "one" `mappend` ("two" `mappend` "tree")
"onetwotree"
ghci> "one" `mappend` "two" `mappend` "tree"
"onetwotree"
ghci> "pang" `mappend` mempty
"pang"
ghci> mconcat [[1,2],[3,6],[9]]
[1,2,3,6,9]
ghci> mempty :: [a]
[]
注意到最后一行我們明白地標(biāo)記出型別。這是因?yàn)槿绻恍?nbsp;mempty 的話,GHCi 不會(huì)知道他是哪一個(gè) instance 的 mempty,所以我們必須清楚說出他是 list instance 的 mempty。我們可以使用一般化的型別 [a],因?yàn)榭盏?list 可以看作是屬于任何型別。
由于 mconcat 有一個(gè)缺省的實(shí)作,我們將某個(gè)型別定義成 Monoid 的型別時(shí)就可以自動(dòng)地得到缺省的實(shí)作。但對(duì)于 list 而言,mconcat 其實(shí)就是 concat。他接受一個(gè)裝有 list 的 list,并把他用 ++ 來扁平化他。
list 的 instance 也遵守 monoid law。當(dāng)我們有好幾個(gè) list 并且用 mappend 來把他們串起來,先后順序并不是很重要,因?yàn)樗麄兌际墙釉谧詈竺?。而且空?list 也表現(xiàn)得如 identity 一樣。注意到 monoid 并不要求 a `mappend` b 等于 b `mappend` a。在 list 的情況下,他們明顯不相等。
ghci> "one" `mappend` "two"
"onetwo"
ghci> "two" `mappend` "one"
"twoone"
這樣并沒有關(guān)系。3 * 5 跟 5 * 3 會(huì)相等只不過是乘法的性質(zhì)而已,但沒有保證所有 monoid 都要遵守。
我們已經(jīng)描述過將數(shù)值表現(xiàn)成一種 monoid 的方式。只要將 * 當(dāng)作二元函數(shù)而 1 當(dāng)作 identity 就好了。而且這不是唯一一種方式,另一種方式是將 + 作為二元函數(shù)而 0 作為 identity。
ghci> 0 + 4
4
ghci> 5 + 0
5
ghci> (1 + 3) + 5
9
ghci> 1 + (3 + 5)
9
他也遵守 monoid law,因?yàn)閷?0 加上其他數(shù)值,都會(huì)是另外一者。而且加法也遵守結(jié)合律。所以現(xiàn)在我們有兩種方式來將數(shù)值表現(xiàn)成 monoid,那要選哪一個(gè)呢?其實(shí)我們不必要強(qiáng)迫定下來,還記得當(dāng)同一種型別有好幾種表現(xiàn)成某個(gè) typeclass 的方式時(shí),我們可以用 newtype 來包裹現(xiàn)有的型別,然后再定義新的 instance。這樣就行了。
Data.Monoid 這個(gè)模塊導(dǎo)出了兩種型別,Product 跟 Sum。Product 定義如下:
newtype Product a = Product { getProduct :: a }
deriving (Eq, Ord, Read, Show, Bounded)
簡單易懂,就是一個(gè)單一型別參數(shù)的 newtype,并 derive 一些性質(zhì)。他的 Monoid 的 instance 長得像這樣:
instance Num a => Monoid (Product a) where
mempty = Product 1
Product x `mappend` Product y = Product (x * y)
mempty 只不過是將 1 包在 Product 中。mappend 則對(duì) Product 的構(gòu)造子做模式匹配,將兩個(gè)取出的數(shù)值相乘后再將結(jié)果放回去。就如你看到的,typeclass 定義前面有 Num a 的條件限制。所以他代表 Product a 對(duì)于所有屬于 Num 的 a 是一個(gè) Monoid。要將 Product a 作為一個(gè) monoid 使用,我們需要用 newtype 來做包裹跟解開的動(dòng)作。
ghci> getProduct $ Product 3 `mappend` Product 9
27
ghci> getProduct $ Product 3 `mappend` mempty
3
ghci> getProduct $ Product 3 `mappend` Product 4 `mappend` Product 2
24
ghci> getProduct . mconcat . map Product $ [3,4,2]
24
這當(dāng)作 Monoid 的一個(gè)演練還不錯(cuò),但并不會(huì)有人覺得這會(huì)比 3 * 9 跟 3 * 1 這種方式來做乘法要好。但我們稍后會(huì)說明盡管像這種顯而易見的定義還是有他方便的地方。
Sum 跟 Product 定義的方式類似,我們也可以用類似的方式操作:
ghci> getSum $ Sum 2 `mappend` Sum 9
11
ghci> getSum $ mempty `mappend` Sum 3
3
ghci> getSum . mconcat . map Sum $ [1,2,3]
6
另一種可以有兩種表示成 monoid 方式的型別是 Bool。第一種方式是將 || 當(dāng)作二元函數(shù),而 False 作為 identity。這樣的意思是只要有任何一個(gè)參數(shù)是 True 他就回傳 True,否則回傳 False。所以如果我們使用 False 作為 identity,他會(huì)在跟 False 做 OR 時(shí)回傳 False,跟 True 做 OR 時(shí)回傳 True。Any 這個(gè) newtype 是 Monoid 的一個(gè) instance,并定義如下:
newtype Any = Any { getAny :: Bool }
deriving (Eq, Ord, Read, Show, Bounded)
他的 instance 長得像這樣:
instance Monoid Any where
mempty = Any False
Any x `mappend` Any y = Any (x || y)
他叫做 Any 的理由是 x `mappend` y 當(dāng)有任何一個(gè)是 True 時(shí)就會(huì)是 True。就算是更多個(gè)用 mappend 串起來的 Any,他也會(huì)在任何一個(gè)是 True 回傳 True。
ghci> getAny $ Any True `mappend` Any False
True
ghci> getAny $ mempty `mappend` Any True
True
ghci> getAny . mconcat . map Any $ [False, False, False, True]
True
ghci> getAny $ mempty `mappend` mempty
False
另一種 Bool 表現(xiàn)成 Monoid 的方式是用 && 作為二元函數(shù),而 True 作為 identity。只有當(dāng)所有都是 True 的時(shí)候才會(huì)回傳 True。下面是他的 newtype 定義:
newtype All = All { getAll :: Bool }
deriving (Eq, Ord, Read, Show, Bounded)
而這是他的 instance:
instance Monoid All where
mempty = All True
All x `mappend` All y = All (x && y)
當(dāng)我們用 mappend 來串起 All 型別的值時(shí),結(jié)果只有當(dāng)所有 mappend 的值是 True 時(shí)才會(huì)是 True:
ghci> getAll $ mempty `mappend` All True
True
ghci> getAll $ mempty `mappend` All False
False
ghci> getAll . mconcat . map All $ [True, True, True]
True
ghci> getAll . mconcat . map All $ [True, True, False]
False
就如乘法跟加法一樣,我們通常寧愿用二元函數(shù)來操作他們也不會(huì)用 newtype 來將他們包起來。不會(huì)將他們包成 Any 或 All 然后用 mappend,mempty 或 mconcat 來操作。通常使用 or 跟 and,他們接受一串 Bool,并只有當(dāng)任意一個(gè)或是所有都是 True 的時(shí)候才回傳 True。
還記得 Ordering 型別嗎?他是比較運(yùn)算之后得到的結(jié)果,包含三個(gè)值:LT,EQ 跟 GT,分別代表小于,等于跟大于:
ghci> 1 `compare` 2
LT
ghci> 2 `compare` 2
EQ
ghci> 3 `compare` 2
GT
針對(duì) list,數(shù)值跟布林值而言,要找出 monoid 的行為只要去查看已經(jīng)定義的函數(shù),然后看看有沒有展現(xiàn)出 monoid 的特性就可以了,但對(duì)于 Ordering,我們就必須要更仔細(xì)一點(diǎn)才能看出來是否是一個(gè) monoid,但其實(shí)他的 Monoid instance 還蠻直覺的:
instance Monoid Ordering where
mempty = EQ
LT `mappend` _ = LT
EQ `mappend` y = y
GT `mappend` _ = GT
這個(gè) instance 定義如下:當(dāng)我們用 mappend 兩個(gè) Ordering 型別的值時(shí),左邊的會(huì)被保留下來。除非左邊的值是 EQ,那我們就會(huì)保留右邊的當(dāng)作結(jié)果。而 identity 就是 EQ。乍看之下有點(diǎn)隨便,但實(shí)際上他是我們比較兩個(gè)英文本時(shí)所用的方法。我們先比較兩個(gè)字母是否相等,如果他們不一樣,那我們就知道那一個(gè)字在字典中會(huì)在前面。而如果兩個(gè)字母相等,那我們就繼續(xù)比較下一個(gè)字母,以此類推。
舉例來說,如果我們字典順序地比較 "ox" 跟 "on" 的話。我們會(huì)先比較兩個(gè)字的首個(gè)字母,看看他們是否相等,然后繼續(xù)比較第二個(gè)字母。我們看到 'x' 是比 'n' 要來得大,所以我們就知道如何比較兩個(gè)字了。而要了解為何 EQ 是 identity,我們可以注意到如果我們?cè)趦蓚€(gè)字中間的同樣位置塞入同樣的字母,那他們之間的字典順序并不會(huì)改變。"oix" 仍然比 "oin" 要大。
很重要的一件事是在 Ordering 的 Monoid 定義里 x `mappend` y 并不等于 y `mappend` x。因?yàn)槌堑谝粋€(gè)參數(shù)是 EQ,不然結(jié)果就會(huì)是第一個(gè)參數(shù)。所以 LT `mappend` GT 等于 LT,然而 GT `mappend` LT 等于 GT。
ghci> LT `mappend` GT
LT
ghci> GT `mappend` LT
GT
ghci> mempty `mappend` LT
LT
ghci> mempty `mappend` GT
GT
所以這個(gè) monoid 在什么情況下會(huì)有用呢?假設(shè)你要寫一個(gè)比較兩個(gè)字串長度的函數(shù),并回傳 Ordering。而且當(dāng)字串一樣長的時(shí)候,我們不直接回傳 EQ,反而繼續(xù)用字典順序比較他們。一種實(shí)作的方式如下:
lengthCompare :: String -> String -> Ordering
lengthCompare x y = let a = length x `compare` length y
b = x `compare` y
in if a == EQ then b else a
我們稱呼比較長度的結(jié)果為 a,而比較字典順序的結(jié)果為 b,而當(dāng)長度一樣時(shí),我們就回傳字典順序。
如果善用我們 Ordering 是一種 monoid 這項(xiàng)知識(shí),我們可以把我們的函數(shù)寫得更簡單些:
import Data.Monoid
lengthCompare :: String -> String -> Ordering
lengthCompare x y = (length x `compare` length y) `mappend`
(x `compare` y)
我們可以試著跑跑看:
ghci> lengthCompare "zen" "ants"
LT
ghci> lengthCompare "zen" "ant"
GT
要記住當(dāng)我們使用 mappend。他在左邊不等于 EQ 的情況下都會(huì)回傳左邊的值。相反地則回傳右邊的值。這也是為什么我們將我們認(rèn)為比較重要的順序放在左邊的參數(shù)。如果我們要繼續(xù)延展這個(gè)函數(shù),要讓他們比較元音的順序,并把這順串行為第二重要,那我們可以這樣修改他:
import Data.Monoid
lengthCompare :: String -> String -> Ordering
lengthCompare x y = (length x `compare` length y) `mappend`
(vowels x `compare` vowels y) `mappend`
(x `compare` y)
where vowels = length . filter (`elem` "aeiou")
我們寫了一個(gè)輔助函數(shù),他接受一個(gè)字串并回傳他有多少元音。他是先用 filter 來把字母濾到剩下 "aeiou",然后再用 length 計(jì)算長度。
ghci> lengthCompare "zen" "anna"
LT
ghci> lengthCompare "zen" "ana"
LT
ghci> lengthCompare "zen" "ann"
GT
在第一個(gè)例子中我們看到長度不同所以回傳 LT,明顯地 "zen" 要短于 "anna"。在第二個(gè)例子中,長度是一樣的,但第二個(gè)字串有比較多的元音,所以結(jié)果仍然是 LT。在第三個(gè)范例中,兩個(gè)長度都相等,他們也有相同個(gè)數(shù)的元音,經(jīng)由字典順序比較后得到 "zen" 比較大。
Ordering 的 monoid 允許我們用不同方式比較事物,并將這些順序也定義了依重要度不同的一個(gè)順序。
我們來看一下 Maybe a 是怎樣有多種方式來表現(xiàn)成 Monoid 的,并且說明哪些是比較有用的。一種將 Maybe a 當(dāng)作 monoid 的方式就是他的 a 也是一個(gè) monoid,而我們將 mappend 實(shí)作成使用包在 Just 里面的值對(duì)應(yīng)的 mappend。并且用 Nothing 當(dāng)作 identity。所以如果我 mappend 兩個(gè)參數(shù)中有一個(gè)是 Nothing。那結(jié)果就會(huì)是另一邊的值。他的 instance 定義如下:
instance Monoid a => Monoid (Maybe a) where
mempty = Nothing
Nothing `mappend` m = m
m `mappend` Nothing = m
Just m1 `mappend` Just m2 = Just (m1 `mappend` m2)
留意到 class constraint。他說明 Maybe a 只有在 a 是 Monoid 的情況下才會(huì)是一個(gè) Monoid。如果我們 mappend 某個(gè)東西跟 Nothing。那結(jié)果就會(huì)是某個(gè)東西。如果我們 mappend 兩個(gè) Just,那 Just 包住的結(jié)果就會(huì) mappended 在一起并放回 Just。我們能這么做是因?yàn)?class constraint 保證了在 Just 中的值是 Monoid。
ghci> Nothing `mappend` Just "andy"
Just "andy"
ghci> Just LT `mappend` Nothing
Just LT
ghci> Just (Sum 3) `mappend` Just (Sum 4)
Just (Sum {getSum = 7})
這當(dāng)你在處理有可能失敗的 monoid 的時(shí)候比較有用。有了這個(gè) instance,我們就不必一一去檢查他們是否失敗,是否是 Nothing 或是 Just,我們可以直接將他們當(dāng)作普通的 monoid。
但如果在 Maybe 中的型別不是 Monoid 呢?注意到在先前的 instance 定義中,唯一有依賴于 monoid 限制的情況就是在 mappend 兩個(gè) Just 的時(shí)候。但如果我們不知道包在 Just 里面的值究竟是不是 monoid,我們根本無法用 mappend 操作他們,所以該怎么辦呢?一種方式就是直接丟掉第二個(gè)值而留下第一個(gè)值。這就是 First a 存在的目的,而這是他的定義:
newtype First a = First { getFirst :: Maybe a }
deriving (Eq, Ord, Read, Show)
我們接受一個(gè) Maybe a 并把他包成 newtype,Monoid 的定義如下:
instance Monoid (First a) where
mempty = First Nothing
First (Just x) `mappend` _ = First (Just x)
First Nothing `mappend` x = x
正如我們說過得,mempty 就是包在 First 中的 Nothing。如果 mappend 的第一個(gè)參數(shù)是 Just,我們就直接忽略第二個(gè)參數(shù)。如果第一個(gè)參數(shù)是 Nothing,那我們就將第二個(gè)參數(shù)當(dāng)作結(jié)果。并不管他究竟是 Just 或是 Nothing:
ghci> getFirst $ First (Just 'a') `mappend` First (Just 'b')
Just 'a'
ghci> getFirst $ First Nothing `mappend` First (Just 'b')
Just 'b'
ghci> getFirst $ First (Just 'a') `mappend` First Nothing
Just 'a'
First 在我們有一大串 Maybe 而且想知道他們之中就竟有沒有 Just 的時(shí)候很有用。可以利用 mconcat:
ghci> getFirst . mconcat . map First $ [Nothing, Just 9, Just 10]
Just 9
如果我們希望定義一個(gè) Maybe a 的 monoid,讓他當(dāng) mappend 的兩個(gè)參數(shù)都是 Just 的時(shí)候?qū)⒌诙€(gè)參數(shù)當(dāng)作結(jié)果。Data.Monoid 中有一個(gè)現(xiàn)成的 Last a,他很像是 First a,只差在 mappend 跟 mconcat 會(huì)保留最后一個(gè)非 Nothing 的值。
ghci> getLast . mconcat . map Last $ [Nothing, Just 9, Just 10]
Just 10
ghci> getLast $ Last (Just "one") `mappend` Last (Just "two")
Just "two"
另一種有趣的 monoid 使用方式就是讓他來幫助我們 fold 一些數(shù)據(jù)結(jié)構(gòu)。到目前為止我們只有 fold list。但 list 并不是唯一一種可以 fold 的數(shù)據(jù)結(jié)構(gòu)。我們幾乎可以 fold 任何一種數(shù)據(jù)結(jié)構(gòu)。像是 tree 也是一種常見的可以 fold 的數(shù)據(jù)結(jié)構(gòu)。
由于有太多種數(shù)據(jù)結(jié)構(gòu)可以 fold 了,所以我們定義了 Foldable 這個(gè) typeclass。就像 Functor 是定義可以 map over 的結(jié)構(gòu)。Foldable 是定義可以 fold 的結(jié)構(gòu)。在 Data.Foldable 中有定義了一些有用的函數(shù),但他們名稱跟 Prelude 中的名稱沖突。所以最好是用 qualified 的方式 import 他們:
import qualified Foldable as F
為了少打一些字,我們將他們 import qualified 成 F。所以這個(gè) typeclass 中定義了哪些函數(shù)呢?有 foldr,foldl,foldr1 跟 foldl1。你會(huì)說我們已經(jīng)知道這些函數(shù)了,他們有什么不一樣的地方嗎?我們來比較一下 Foldable 中的 foldr 跟 Prelude 中的 foldr 的型別異同:
ghci> :t foldr
foldr :: (a -> b -> b) -> b -> [a] -> b
ghci> :t F.foldr
F.foldr :: (F.Foldable t) => (a -> b -> b) -> b -> t a -> b
盡管 foldr 接受一個(gè) list 并將他 fold 起來,Data.Foldable 中的 foldr 接受任何可以 fold 的型別。并不只是 list。 而兩個(gè) foldr 對(duì)于 list 的結(jié)果是相同的:
ghci> foldr (*) 1 [1,2,3]
6
ghci> F.foldr (*) 1 [1,2,3]
6
那有哪些數(shù)據(jù)結(jié)構(gòu)支持 fold 呢?首先我們有 Maybe:
ghci> F.foldl (+) 2 (Just 9)
11
ghci> F.foldr (||) False (Just True)
True
但 fold 一個(gè) Maybe 并沒什么新意。畢竟當(dāng)他是 Just 的時(shí)候表現(xiàn)得像是只有單一元素的 list,而當(dāng)他是 Nothing 的時(shí)候就像是空的 list 一樣。所以我們來看一些比較復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。
還記得 Making Our Own Types and Typeclass 章節(jié)中的樹狀的數(shù)據(jù)結(jié)構(gòu)嗎?我們是這樣定義的:
data Tree a = Empty | Node a (Tree a) (Tree a) deriving (Show, Read, Eq)
我們說一棵樹要不就是一棵空的樹要不然就是一個(gè)包含值的節(jié)點(diǎn),并且還指向另外兩棵樹。定義他之后,我們將他定義成 Functor 的 instance,因此可以 fmap 他?,F(xiàn)在我們要將他定義成 Foldable 的 instance,這樣我們就可以 fold 他。要定義成 Foldable 的一種方式就是實(shí)作 foldr。但另一種比較簡單的方式就是實(shí)作 foldMap,他也屬于 Foldable typeclass。foldMap 的型別如下:
foldMap :: (Monoid m, Foldable t) => (a -> m) -> t a -> m
第一個(gè)參數(shù)是一個(gè)函數(shù),這個(gè)函數(shù)接受 foldable 數(shù)據(jù)結(jié)構(gòu)中包含的元素的型別,并回傳一個(gè) monoid。他第二個(gè)參數(shù)是一個(gè) foldable 的結(jié)構(gòu),并包含型別 a 的元素。他將第一個(gè)函數(shù)來 map over 這個(gè) foldable 的結(jié)構(gòu),因此得到一個(gè)包含 monoid 的 foldable 結(jié)構(gòu)。然后用 mappend 來簡化這些 monoid,最后得到單一的一個(gè) monoid。這個(gè)函數(shù)聽起來不太容易理解,但我們下面會(huì)看到他其實(shí)很容易實(shí)作。而且好消息是只要實(shí)作了這個(gè)函數(shù)就可以讓我們的函數(shù)成為 Foldable。所以我們只要實(shí)作某個(gè)型別的 foldMap,我們就可以得到那個(gè)型別的 foldr 跟 foldl。
這就是我們?nèi)绾味x Tree 成為 Foldable 的:
instance F.Foldable Tree where
foldMap f Empty = mempty
foldMap f (Node x l r) = F.foldMap f l `mappend`
f x `mappend`
F.foldMap f r
我們是這樣思考的:如果我們寫一個(gè)函數(shù),他接受樹中的一個(gè)元素并回傳一個(gè) monoid,那我們要怎么簡化整棵樹到只有單一一個(gè) monoid?當(dāng)我們?cè)趯?duì)樹做 fmap 的時(shí)候,我們將那函數(shù)套用至節(jié)點(diǎn)上,并遞歸地套用至左子樹以及右子樹。這邊我們不只是 map 一個(gè)函數(shù)而已,我們還要求要把結(jié)果用 mappend 簡化成只有單一一個(gè) monoid 值。首先我們考慮樹為空的情形,一棵沒有值也沒有子樹的情形。由于沒有值我們也沒辦法將他套用上面轉(zhuǎn)換成 monoid 的函數(shù),所以當(dāng)樹為空的時(shí)候,結(jié)果應(yīng)該要是 mempty。
在非空節(jié)點(diǎn)的情形下比較有趣,他包含一個(gè)值跟兩棵子樹。在這種情況下,我們遞歸地做 foldMap,用 f 來套用到左子樹跟右子樹上。要記住我們的 foldMap 只會(huì)得到單一的 monoid 值。我們也會(huì)套用 f 到節(jié)點(diǎn)中的值。這樣我們就得到三個(gè) monoid 值,有兩個(gè)來自簡化子樹的結(jié)果,還有一個(gè)是套用 f 到節(jié)點(diǎn)中的值的結(jié)果。而我們需要將這三個(gè)值集成成單一個(gè)值。要達(dá)成這個(gè)目的我們使用 mappend,而且自然地會(huì)想到照左子樹,節(jié)點(diǎn)值以及右子樹的順序來簡化。
注意到我們并不一定要提供一個(gè)將普通值轉(zhuǎn)成 monoid 的函數(shù)。我們只是把他當(dāng)作是 foldMap 的參數(shù),我們要決定的只是如何套用那個(gè)函數(shù),來把得到的 monoid 們簡化成單一結(jié)果。
現(xiàn)在我們有樹的 Foldable instance,而 foldr 跟 foldl 也有缺省的實(shí)作了??紤]下面這棵樹:
testTree = Node 5
(Node 3
(Node 1 Empty Empty)
(Node 6 Empty Empty)
)
(Node 9
(Node 8 Empty Empty)
(Node 10 Empty Empty)
)
他的 root 是 5,而他左邊下來分別是 3,再來是 1 跟 6。而右邊下來是 9,再來是 8 跟 10。有了 Foldable 的定義,我們就能像對(duì) list 做 fold 一樣對(duì)樹做 fold:
ghci> F.foldl (+) 0 testTree
42
ghci> F.foldl (*) 1 testTree
64800
foldMap 不只是定義 Foldable 新的 instance 有用。他也對(duì)簡化我們的結(jié)構(gòu)至單一 monoid 值有用。舉例來說,如果我們想要知道我們的樹中有沒有 3,我們可以這樣做:
ghci> getAny $ F.foldMap (\x -> Any $ x == 3) testTree
True
這邊 \x -> Any $ x == 3 是一個(gè)接受一個(gè)數(shù)值并回傳一個(gè) monoid 的函數(shù),也就是一個(gè)包在 Any 中的 Bool。foldMap 將這個(gè)函數(shù)套用至樹的每一個(gè)節(jié)點(diǎn),并把結(jié)果用 mappend 簡化成單一 monoid。如果我們這樣做:
ghci> getAny $ F.foldMap (\x -> Any $ x > 15) testTree
False
經(jīng)過套用 lambda 之后我們所有的節(jié)點(diǎn)都會(huì)是 Any False。但 mappend 必須要至少吃到一個(gè) True 才能讓最后的結(jié)果變成 True。這也是為什么結(jié)果會(huì)是 False,因?yàn)槲覀儤渲兴械闹刀夹∮诘扔?nbsp;15。
我們也能將 foldMap 配合 \x -> [x] 使用來將我們的樹轉(zhuǎn)成 list。經(jīng)過套用那個(gè)函數(shù)后,所有節(jié)點(diǎn)都變成包含單一元素的 list。最后用 mappend 將這些單一元素的 list 轉(zhuǎn)成一個(gè)裝有全部元素的 list:
ghci> F.foldMap (\x -> [x]) testTree
[1,3,6,5,8,9,10]
這個(gè)小技巧并不限于樹而已,他可以被套用在任何 Foldable 上。
更多建議: