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

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

Functors, Applicative Functors 與 Monoids

Haskell 的一些特色,像是純粹性,高階函數(shù),algebraic data types,typeclasses,這些讓我們可以從更高的角度來看到 polymorphism 這件事。不像 OOP 當(dāng)中需要從龐大的型態(tài)階層來思考。我們只需要看看手邊的型態(tài)的行為,將他們跟適當(dāng)?shù)?typeclass 對應(yīng)起來就可以了。像 Int 的行為跟很多東西很像。好比說他可以比較相不相等,可以從大到小排列,也可以將他們一一窮舉出來。

Typeclass 的運用是很隨意的。我們可以定義自己的資料型態(tài),然后描述他可以怎樣被操作,跟 typeclass 關(guān)聯(lián)起來便定義了他的行為。由于 Haskell 強大的型態(tài)系統(tǒng),這讓我們只要讀函數(shù)的型態(tài)宣告就可以知道很多資訊。typeclass 可以定義得很抽象很 general。我們之前有看過 typeclass 定義了可以比較兩個東西是否相等,或是定義了可以比較兩個東西的大小。這些是既抽象但又描述簡潔的行為,但我們不會認(rèn)為他們有什么特別之處,因為我們時常碰到他們。最近我們看過了 functor,基本上他們是一群可以被 map over 的物件。這是其中一個例子能夠抽象但又漂亮地描述行為。在這一章中,我們會詳加闡述 functors,并會提到比較強一些的版本,也就是 applicative functors。我們也會提到 monoids。

溫習(xí) Functors

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

我們已經(jīng)在之前的章節(jié)提到 functors。如果你還沒讀那個章節(jié),也許你應(yīng)該先去看看。或是你直接假裝你已經(jīng)讀過了。

來快速復(fù)習(xí)一下:Functors 是可以被 map over 的物件,像是 lists,Maybe,trees 等等。在 Haskell 中我們是用 Functor 這個 typeclass 來描述他。這個 typeclass 只有一個 method,叫做 fmap,他的型態(tài)是 fmap :: (a -> b) -> f a -> f b。這型態(tài)說明了如果給我一個從 a 映到 b 的函數(shù),以及一個裝了 a 的盒子,我會回給你一個裝了 b 的盒子。就好像用這個函數(shù)將每個元素都轉(zhuǎn)成 b 一樣

*給一點建議*。這盒子的比喻嘗試讓你抓到些 functors 是如何運作的感覺。在之后我們也會用相同的比喻來比喻 applicative functors 跟 monads。在多數(shù)情況下這種比喻是恰當(dāng)?shù)模灰^度引申,有些 functors 是不適用這個比喻的。一個比較正確的形容是 functors 是一個計算語境(computational context)。這個語境可能是這個 computation 可能帶有值,或是有可能會失?。ㄏ?``Maybe`` 跟 ``Either a``),或是他可能有多個值(像 lists),等等。

如果一個 type constructor 要是 Functor 的 instance,那他的 kind 必須是 * -> *,這代表他必須剛好接受一個 type 當(dāng)作 type parameter。像是 Maybe 可以是 Functor 的一個 instance,因為他接受一個 type parameter,來做成像是 Maybe Int,或是 Maybe String。如果一個 type constructor 接受兩個參數(shù),像是 Either,我們必須給他兩個 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 的是固定的一部分,因為 Either a 只恰好接受一個 type parameter,但 Either 則要接受兩個 type parameters。這樣 fmap 的型態(tài)變成 fmap :: (b -> c) -> Either b -> Either c,這不太合理。

我們知道有許多型態(tài)都是 Functor 的 instance,像是 [],Maybe,Either a 以及我們自己寫的 Tree。我們也看到了如何用一個函數(shù) map 他們。在這一章節(jié),我們再多舉兩個例子,也就是 IO(->) r。

如果一個值的型態(tài)是 IO String,他代表的是一個會被計算成 String 結(jié)果的 I/O action。我們可以用 do syntax 來把結(jié)果綁定到某個名稱。我們之前把 I/O action 比喻做長了腳的盒子,會到真實世界幫我們?nèi)∫恍┲祷貋?。我們可以檢視他們?nèi)×耸裁粗?,但一旦看過,我們必須要把值放回盒子中。用這個比喻,IO 的行為就像是一個 functor。

我們來看看 IO 是怎么樣的一個 Functor instance。當(dāng)我們 fmap 用一個 function 來 map over I/O action 時,我們會想要拿回一個裝著已經(jīng)用 function 映射過值的 I/O action。

instance Functor IO where
    fmap f action = do
        result <- action
        return (f result)

對一個 I/O action 做 map over 動作的結(jié)果仍會是一個 I/O action,所以我們才用 do syntax 來把兩個 I/O action 黏成一個。在 fmap 的實作中,我們先執(zhí)行了原本傳進的 I/O action,并把結(jié)果綁定成 result。然后我們寫了 return (f result)。return 就如你所知道的,是一個只會回傳包了你傳給他東西的 I/O action。還有一個 do block 的回傳值一定是他最后一個 I/O action 的回傳值。這也是為什么我們需要 return。其實他只是回傳包了 f result 的 I/O action。

我們可以再多實驗一下來找到些感覺。來看看這段 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!"  

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

就像我們用 fmap reverse 來 map over Just "blah" 會得到 Just "halb",我們也可以 fmap reverse 來 map over getLine。getLine 是一個 I/O action,他的 type 是 IO String,而用 reverse 來 map over 他會回傳一個取回一個字串并 reverse 他的 I/O action。就像我們 apply 一個 function 到一個 Maybe 一樣,我們也可以 apply 一個 function 到一個 IO,只是這個 IO 會跑去外面拿回某些值。然后我們把結(jié)果用 <- 綁定到某個名稱,而這個名稱綁定的值是已經(jīng) reverse 過了。

fmap (++"!") getLine 這個 I/O action 表現(xiàn)得就像 getLine,只是他的結(jié)果多了一個 "!" 在最后。

如果我們限縮 fmapIO 型態(tài)上,那 fmap 的型態(tài)是 fmap :: (a -> b) -> IO a -> IO b。fmap 接受一個函數(shù)跟一個 I/O action,并回傳一個 I/O action 包含了已經(jīng) apply 過 function 的結(jié)果。

如果你曾經(jīng)注意到你想要將一個 I/O action 綁定到一個名稱上,只是為了要 apply 一個 function。你可以考慮使用 fmap,那會更漂亮地表達(dá)這件事?;蛘吣阆胍獙?functor 中的資料做 transformation,你可以先將你要用的 function 寫在 top level,或是把他作成一個 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 合成了一個 function,他接受一個字串,將他轉(zhuǎn)成大寫,然后反過來,再用 intersperse '-' 安插'-'。他是比較漂亮版本的 (\xs -> intersperse '-' (reverse (map toUpper xs)))

另一個 Functor 的案例是 (->) r,只是我們先前沒有注意到。你可能會困惑到底 (->) r 究竟代表什么?一個 r -> a 的型態(tài)可以寫成 (->) r a,就像是 2 + 3 可以寫成 (+) 2 3 一樣。我們可以從一個不同的角度來看待 (->) r a,他其實只是一個接受兩個參數(shù)的 type constructor,好比 Either。但記住我們說過 Functor 只能接受一個 type constructor。這也是為什么 (->) 不是 Functor 的一個 instance,但 (->) r 則是。如果程式的語法允許的話,你也可以將 (->) r 寫成 (r ->)。就如 (2+) 代表的其實是 (+) 2。至于細(xì)節(jié)是如何呢?我們可以看看 Control.Monad.Instances。

我們通常說一個接受任何東西以及回傳隨便一個東西的函數(shù)型態(tài)是 ``a -> b``。``r -> a`` 是同樣意思,只是把符號代換了一下。
instance Functor ((->) r) where  
    fmap f g = (\x -> f (g x))  

如果語法允許的話,他可以被寫成

instance Functor (r ->) where  
    fmap f g = (\x -> f (g x))  

但其實是不允許的,所以我們必須寫成第一種的樣子。

首先我們來看看 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 -> ar -> b。則我們得到 fmap :: (a -> b) -> (r -> a) -> (r -> b)。

從上面的結(jié)果看到將一個 function map over 一個 function 會得到另一個 function,就如 map over 一個 function 到 Maybe 會得到一個 Maybe,而 map over 一個 function 到一個 list 會得到一個 list。而 fmap :: (a -> b) -> (r -> a) -> (r -> b) 告訴我們什么?他接受一個從 ab 的 function,跟一個從 ra 的 function,并回傳一個從 rb 的 function。這根本就是 function composition。把 r -> a 的輸出接到 a -> b 的輸入,的確是 function composition 在做的事。如果你再仔細(xì)看看 instance 的定義,會發(fā)現(xiàn)真的就是一個 function composition。

instance Functor ((->) r) where  
    fmap = (.)  

這很明顯就是把 fmap 當(dāng) composition 在用??梢杂?:m + Control.Monad.Instances 把模組裝載進來,并做一些嘗試。

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"  

我們呼叫 fmap 的方式是 infix 的方式,這跟 . 很像。在第二行,我們把 (*3) map over 到 (+100) 上,這會回傳一個先把輸入值 (+100)(*3) 的 function,我們再用 1 去呼叫他。

到這邊為止盒子的比喻還適用嗎?如果你硬是要解釋的話還是解釋得通。當(dāng)我們將 fmap (+3) map over Just 3 的時候,對于 Maybe 我們很容易把他想成是裝了值的盒子,我們只是對盒子里面的值 (+3)。但對于 fmap (*3) (+100) 呢?你可以把 (+100) 想成是一個裝了值的盒子。有點像把 I/O action 想成長了腳的盒子一樣。對 (+100) 使用 fmap (*3) 會產(chǎn)生另一個表現(xiàn)得像 (+100) 的 function。只是在算出值之前,會再多計算 (*3)。這樣我們可以看出來 fmap 表現(xiàn)得就像 . 一樣。

fmap 等同于 function composition 這件事對我們來說并不是很實用,但至少是一個有趣的觀點。這也讓我們打開視野,看到盒子的比喻不是那么恰當(dāng),functors 其實比較像 computation。function 被 map over 到一個 computation 會產(chǎn)生經(jīng)由那個 function 映射過后的 computation。

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

在我們繼續(xù)看 fmap 該遵守的規(guī)則之前,我們再看一次 fmap 的型態(tài),他是 fmap :: (a -> b) -> f a -> f b。很明顯我們是在討論 Functor,所以為了簡潔,我們就不寫 (Functor f) => 的部份。當(dāng)我們在學(xué) curry 的時候,我們說過 Haskell 的 function 實際上只接受一個參數(shù)。一個型態(tài)是 a -> b -> c 的函數(shù)實際上是接受 a 然后回傳 b -> c,而 b -> c 實際上接受一個 b 然后回傳一個 c。如果我們用比較少的參數(shù)呼叫一個函數(shù),他就會回傳一個函數(shù)需要接受剩下的參數(shù)。所以 a -> b -> c 可以寫成 a -> (b -> c)。這樣 curry 可以明顯一些。

同樣的,我們可以不要把 fmap 想成是一個接受 function 跟 functor 并回傳一個 function 的 function。而是想成一個接受 function 并回傳一個新的 function 的 function,回傳的 function 接受一個 functor 并回傳一個 functor。他接受 a -> b 并回傳 f a -> f b。這動作叫做 lifting。我們用 GHCI 的 :t 來做的實驗。

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) 接受一個 functor f,并回傳一個基于數(shù)字的 functor。那個 functor 可以是 list,可以是 Maybe,可以是 Either String。fmap (replicate 3) 可以接受一個基于任何型態(tài)的 functor,并回傳一個基于 list 的 functor。

當(dāng)我們提到 functor over numbers 的時候,你可以想像他是一個 functor 包含有許多數(shù)字在里面。前面一種說法其實比較正確,但后面一種說法比較容易讓人理解。

這樣的觀察在我們只有綁定一個部份套用的函數(shù),像是 fmap (++"!"),的時候會顯得更清楚,

你可以把 fmap 想做是一個函數(shù),他接受另一個函數(shù)跟一個 functor,然后把函數(shù)對 functor 每一個元素做映射,或你可以想做他是一個函數(shù),他接受一個函數(shù)并把他 lift 到可以在 functors 上面操作。兩種想法都是正確的,而且在 Haskell 中是等價。

fmap (replicate 3) :: (Functor f) => f a -> f [a] 這樣的型態(tài)代表這個函數(shù)可以運作在任何 functor 上。至于確切的行為則要看究竟我們操作的是什么樣的 functor。如果我們是用 fmap (replicate 3) 對一個 list 操作,那我們會選擇 fmap 針對 list 的實作,也就是只是一個 map。如果我們是碰到 Maybe a。那他在碰到 Just 型態(tài)的時候,會對里面的值套用 replicate 3。而碰到 Nothing 的時候就回傳 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。一個東西要成為 functor,必須要遵守某些定律。不管任何一個 functor 都被要求具有某些性質(zhì)。他們必須是能被 map over 的。對他們呼叫 fmap 應(yīng)該是要用一個函數(shù) map 每一個元素,不多做任何事情。這些行為都被 functor laws 所描述。對于 Functor 的 instance 來說,總共兩條定律應(yīng)該被遵守。不過他們不會在 Haskell 中自動被檢查,所以你必須自己確認(rèn)這些條件。

functor law 的第一條說明,如果我們對 functor 做 map id,那得到的新的 functor 應(yīng)該要跟原來的一樣。如果寫得正式一點,他代表 fmap id = id?;旧纤褪钦f對 functor 呼叫 fmap id,應(yīng)該等同于對 functor 呼叫 id 一樣。畢竟 id 只是 identity function,他只會把參數(shù)照原樣丟出。他也可以被寫成 \x -> x。如果我們對 functor 的概念就是可以被 map over 的物件,那 fmap id = id 的性就顯而易見。

我們來看看這個定律的幾個案例:

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  

如果我們看看 Maybefmap 的實作,我們不難發(fā)現(xiàn)第一定律為何被遵守。

instance Functor Maybe where  
    fmap f (Just x) = Just (f x)  
    fmap f Nothing = Nothing  

我們可以想像在 f 的位置擺上 id。我們看到 fmap id 拿到 Just x 的時候,結(jié)果只不過是 Just (id x),而 id 有只回傳他拿到的東西,所以可以知道 Just (id x) 等價于 Just x。所以說我們可以知道對 Maybe 中的 Justid 去做 map over 的動作,會拿回一樣的值。

而將 id map over Nothing 會拿回 Nothing 并不稀奇。所以從這兩個 fmap 的實作,我們可以看到的確 fmap id = id 有被遵守。

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

第二定律描述說先將兩個函數(shù)合成并將結(jié)果 map over 一個 functor 的結(jié)果,應(yīng)該跟先將第一個函數(shù) map over 一個 functor,再將第二個函數(shù) map over 那個 functor 的結(jié)果是一樣的。正式地寫下來的話就是 fmap (f . g) = fmap f . fmap g。或是用另外一種寫法,對于任何一個 functor F,下面這個式子應(yīng)該要被遵守:fmap (f . g) F = fmap f (fmap g F)。

如果我們能夠證明某個型別遵守兩個定律,那我們就可以保證他跟其他 functor 對于映射方面都擁有相同的性質(zhì)。我們知道如果對他用 fmap,我們知道不會有除了 mapping 以外的事會發(fā)生,而他就僅僅會表現(xiàn)成某個可以被 map over 的東西。也就是一個 functor。你可以再仔細(xì)檢視 fmap 對于某些型別的實作來了解第二定律。正如我們先前對 Maybe 檢視第一定律一般。

如果你需要的話,我們能在這邊演練一下 Maybe 是如何遵守第二定律的。首先 fmap (f . g) 來 map over Nothing 的話,我們會得到 Nothing。因為用任何函數(shù)來 fmap Nothing 的話都會回傳 Nothing。如果我們 fmap f (fmap g Nothing),我們會得到 Nothing??梢钥吹疆?dāng)面對 Nothing 的時候,Maybe 很顯然是遵守第二定律的。 那對于 Just something 呢?如果我們使用 fmap (f . g) (Just x) 的話,從實作的程式碼中我可以看到 Just ((f . g ) x),也就是 Just (f (g x))。如果我們使用 fmap f (fmap g (Just x)) 的話我們可以從實作知道 fmap g (Just x) 會是 Just (g x)。fmap f (fmap g (Just x))fmap f (Just (g x)) 相等。而從實作上這又會相等于 Just (f (g x))。

如果你不太理解這邊的說明,別擔(dān)心。只要確定你了解什么是函數(shù)合成就好。在多數(shù)的情況下你可以直覺地對應(yīng)到這些型別表現(xiàn)得就像 containers 或函數(shù)一樣?;蚴且部梢該Q種方法,只要多嘗試對型別中不同的值做操作你就可以看看型別是否有遵守定律。

我們來看一些經(jīng)典的例子。這些型別建構(gòu)子雖然是 Functor 的 instance,但實際上他們并不是 functor,因為他們并不遵守這些定律。我們來看看其中一個型別。

data CMaybe a = CNothing | CJust Int a deriving (Show)      

C 這邊代表的是計數(shù)器。他是一種看起來像是 Maybe a 的型別,只差在 Just 包含了兩個 field 而不是一個。在 CJust 中的第一個 field 是 Int,他是扮演計數(shù)器用的。而第二個 field 則為型別 a,他是從型別參數(shù)來的,而他確切的型別當(dāng)然會依據(jù)我們選定的 CMaybe a 而定。我們來對他作些操作來獲得些操作上的直覺吧。

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,那第一個 field 是整數(shù),而第二個 field 可以為任何型別。我們來定義一個 Functor 的 instance,這樣每次我們使用 fmap 的時候,函數(shù)會被套用在第二個 field,而第一個 field 會被加一。

instance Functor CMaybe where  
    fmap f CNothing = CNothing  
    fmap f (CJust counter x) = CJust (counter+1) (f x)  

這種定義方式有點像是 Maybe 的定義方式,只差在當(dāng)我們使用 fmap 的時候,如果碰到的不是空值,那我們不只會套用函數(shù),還會把計數(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  

這些會遵守 functor laws 嗎?要知道有不遵守的情形,只要找到一個反例就好了。

ghci> fmap id (CJust 0 "haha")  
CJust 1 "haha"  
ghci> id (CJust 0 "haha")  
CJust 0 "haha"  

我們知道 functor law 的第一定律描述當(dāng)我們用 id 來 map over 一個 functor 的時候,他的結(jié)果應(yīng)該跟只對 functor 呼叫 id 的結(jié)果一樣。但我們可以看到這個例子中,這對于 CMaybe 并不遵守。盡管他的確是 Functor typeclass 的一個 instace。但他并不遵守 functor law 因此不是一個 functor。如果有人使用我們的 CMaybe 型別,把他當(dāng)作 functor 用,那他就會期待 functor laws 會被遵守。但 CMaybe 并沒辦法滿足,便會造成錯誤的程式。當(dāng)我們使用一個 functor 的時候,函數(shù)合成跟 map over 的先后順序不應(yīng)該有影響。但對于 CMaybe 他是有影響的,因為他紀(jì)錄了被 map over 的次數(shù)。如果我們希望 CMaybe 遵守 functor law,我們必須要讓 Int 欄位在做 fmap 的時候維持不變。

乍看之下 functor laws 看起來不是很必要,也容易讓人搞不懂,但我們知道如果一個型別遵守 functor laws,那我們就能對他作些基本的假設(shè)。如果遵守了 functor laws,我們知道對他做 fmap 不會做多余的事情,只是用一個函數(shù)做映射而已。這讓寫出來的程式碼足夠抽象也容易擴展。因為我們可以用定律來推論型別的行為。

所有在標(biāo)準(zhǔn)函式庫中的 Functor 的 instance 都遵守這些定律,但你可以自己檢查一遍。下一次你定義一個型別為 Functor 的 instance 的時候,花點時間確認(rèn)他確實遵守 functor laws。一旦你操作過足夠多的 functors 時,你就會獲得直覺,知道他們會有什么樣的性質(zhì)跟行為。而且 functor laws 也會覺得顯而易見。但就算沒有這些直覺,你仍然可以一行一行地來找看看有沒有反例讓這些定律失效。

我們可以把 functor 看作輸出具有 context 的值。例如說 Just 3 就是輸出 3,但他又帶有一個可能沒有值的 context。[1,2,3] 輸出三個值,1,23,同時也帶有可能有多個值或沒有值的 context。(+3) 則會帶有一個依賴于參數(shù)的 context。

如果你把 functor 想做是輸出值這件事,那你可以把 map over 一個 functor 這件事想成在 functor 輸出的后面再多加一層轉(zhuǎn)換。當(dāng)我們做 fmap (+3) [1,2,3] 的時候,我們是把 (+3) 接到 [1,2,3] 后面,所以當(dāng)我們檢視任何一個 list 的輸出的時候,(+3) 也會被套用在上面。另一個例子是對函數(shù)做 map over。當(dāng)我們做 fmap (+3) (*3),我們是把 (+3) 這個轉(zhuǎn)換套用在 (*3) 后面。這樣想的話會很自然就會把 fmap 跟函數(shù)合成關(guān)聯(lián)起來(fmap (+3) (*3) 等價于 (+3) . (*3),也等價于 \x -> ((x*3)+3)),畢竟我們是接受一個函數(shù) (*3) 然后套用 (+3) 轉(zhuǎn)換。最后的結(jié)果仍然是一個函數(shù),只是當(dāng)我們喂給他一個數(shù)字的時候,他會先乘上三然后做轉(zhuǎn)換加上三。這基本上就是函數(shù)合成在做的事。

Applicative functors

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

在這個章節(jié)中,我們會學(xué)到 applicative functors,也就是加強版的 functors,在 Haskell 中是用在 Control.Applicative 中的 Applicative 這個 typeclass 來定義的。

你還記得 Haskell 中函數(shù)預(yù)設(shè)就是 Curried 的,那代表接受多個參數(shù)的函數(shù)實際上是接受一個參數(shù)然后回傳一個接受剩余參數(shù)的函數(shù),以此類推。如果一個函數(shù)的型別是 a -> b -> c,我們通常會說這個函數(shù)接受兩個參數(shù)并回傳 c,但他實際上是接受 a 并回傳一個 b -> c 的函數(shù)。這也是為什么我們可以用 (f x) y 的方式呼叫 f x y。這個機制讓我們可以 partially apply 一個函數(shù),可以用比較少的參數(shù)呼叫他們??梢宰龀梢粋€函數(shù)再喂給其他函數(shù)。

到目前為止,當(dāng)我們要對 functor map over 一個函數(shù)的時候,我們用的函數(shù)都是只接受一個參數(shù)的。但如果我們要 map 一個接受兩個參數(shù)的函數(shù)呢?我們來看幾個具體的例子。如果我們有 Just 3 然后我們做 fmap (*) (Just 3),那我們會獲得什么樣的結(jié)果?從 MaybeFunctor 的 instance 實作來看,我們知道如果他是 Just something,他會對在 Just 中的 something 做映射。因此當(dāng) fmap (*) (Just 3) 會得到 Just ((*) 3),也可以寫做 Just (* 3)。我們得到了一個包在 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 到一個包含許多字元的 list 呢?他的型別是 (Ord a) => a -> a -> Ordering,我們會得到包含許多 Char -> Ordering 型別函數(shù)的 list,因為 compare 被 partially apply 到 list 中的字元。他不是包含許多 (Ord a) => a -> Ordering 的函數(shù),因為第一個 a 碰到的型別是 Char,所以第二個 a 也必須是 Char

我們看到如何用一個多參數(shù)的函數(shù)來 map functor,我們會得到一個包含了函數(shù)的 functor。那現(xiàn)在我們能對這個包含了函數(shù)的 functor 做什么呢?我們能用一個吃這些函數(shù)的函數(shù)來 map over 這個 functor,這些在 functor 中的函數(shù)都會被當(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]  

但如果我們的有一個 functor 里面是 Just (3 *) 還有另一個 functor 里面是 Just 5,但我們想要把第一個 Just (3 *) map over Just 5 呢?如果是普通的 functor,那就沒救了。因為他們只允許 map 一個普通的函數(shù)。即使我們用 \f -> f 9 來 map 一個裝了很多函數(shù)的 functor,我們也是使用了普通的函數(shù)。我們是無法單純用 fmap 來把包在一個 functor 的函數(shù) map 另一個包在 functor 中的值。我們能用模式匹配 Just 來把函數(shù)從里面抽出來,然后再 map Just 5,但我們是希望有一個一般化的作法,對任何 functor 都有效。

我們來看看 Applicative 這個 typeclass。他位在 Control.Applicative 中,在其中定義了兩個函數(shù) pure<*>。他并沒有提供預(yù)設(shè)的實作,如果我們想使用他必須要為他們 applicative functor 的實作。typeclass 定義如下:

class (Functor f) => Applicative f where  
    pure :: a -> f a  
    (<*>) :: f (a -> b) -> f a -> f b  

這簡簡單單的三行可以讓我們學(xué)到不少。首先來看第一行。他開啟了 Applicative 的定義,并加上 class contraint。描述了一個型別構(gòu)造子要是 Applicative,他必須也是 Functor。這就是為什么我們說一個型別構(gòu)造子屬于 Applicative 的話,他也會是 Functor,因此我們能對他使用 fmap。

第一個定義的是 pure。他的型別宣告是 pure :: a -> f a。f 代表 applicative functor 的 instance。由于 Haskell 有一個優(yōu)秀的型別系統(tǒng),其中函數(shù)又是將一些參數(shù)映射成結(jié)果,我們可以從型別宣告中讀出許多訊息。pure 應(yīng)該要接受一個值,然后回傳一個包含那個值的 applicative functor。我們這邊是用盒子來作比喻,即使有一些比喻不完全符合現(xiàn)實的情況。盡管這樣,a -> f a 仍有許多豐富的資訊,他確實告訴我們他會接受一個值并回傳一個 applicative functor,里面裝有結(jié)果。

對于 pure 比較好的說法是把一個普通值放到一個預(yù)設(shè)的 context 下,一個最小的 context 但仍然包含這個值。

<*> 也非常有趣。他的型別是 f (a -> b) -> f a -> f b。這有讓你聯(lián)想到什么嗎?沒錯!就是 fmap :: (a -> b) -> f a -> f b。他有點像加強版的 fmap。然而 fmap 接受一個函數(shù)跟一個 functor,然后套用 functor 之中的函數(shù)。<*> 則是接受一個裝有函數(shù)的 functor 跟另一個 functor,然后取出第一個 functor 中的函數(shù)將他對第二個 functor 中的值做 map。

我們來看看 MaybeApplicative 實作:

instance Applicative Maybe where  
    pure = Just  
    Nothing <*> _ = Nothing  
    (Just f) <*> something = fmap f something  

從 class 的定義我們可以看到 f 作為 applicative functor 會接受一個具體型別當(dāng)作參數(shù),所以我們是寫成 instance Applicative Maybe where 而不是寫成 instance Applicative (Maybe a) where

首先看到 pure。他只不過是接受一個東西然后包成 applicative functor。我們寫成 pure = Just 是因為 Just 不過就是一個普通函數(shù)。我們其實也可以寫成 pure x = Just x。

接著我們定義了 <*>。我們無法從 Nothing 中抽出一個函數(shù),因為 Nothing 并不包含一個函數(shù)。所以我們說如果我們要嘗試從 Nothing 中取出一個函數(shù),結(jié)果必定是 Nothing。如果你看看 Applicative 的定義,你會看到他有 Functor 的限制,他代表 <*> 的兩個參數(shù)都會是 functors。如果第一個參數(shù)不是 Nothing,而是一個裝了函數(shù)的 Just,而且我們希望將這個函數(shù)對第二個參數(shù)做 map。這個也考慮到第二個參數(shù)是 Nothing 的情況,因為 fmap 任何一個函數(shù)至 Nothing 會回傳 Nothing

對于 Maybe 而言,如果左邊是 Just,那 <*> 會從其中抽出了一個函數(shù)來 map 右邊的值。如果有任何一個參數(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) 在這個 case 下是一樣的。如果你是在 applicative context 底下跟 Maybe 打交道的話請用 pure,要不然就用 Just。前四個輸入展示了函數(shù)是如何被取出并做 map 的動作,但在這個 case 底下,他們同樣也可以用 unwrap 函數(shù)來 map over functors。最后一行比較有趣,因為我們試著從 Nothing 取出函數(shù)并將他 map 到某個值。結(jié)果當(dāng)然是 Nothing。

對于普通的 functors,你可以用一個函數(shù) map over 一個 functors,但你可能沒辦法拿到結(jié)果。而 applicative functors 則讓你可以用單一一個函數(shù)操作好幾個 functors??纯聪旅嬉欢纬淌酱a:

ghci> pure (+) <*> Just 3 <*> Just 5  
Just 8  
ghci> pure (+) <*> Just 3 <*> Nothing  
Nothing  
ghci> pure (+) <*> Nothing <*> Just 5  
Nothing  

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

究竟我們寫了些什么?我們來一步步看一下。<*> 是 left-associative,也就是說 pure (+) <*> Just 3 <*> Just 5 可以寫成 (pure (+) <*> Just 3) <*> Just 5。首先 + 是擺在一個 functor 中,在這邊剛好他是一個 Maybe。所以首先,我們有 pure (+),他等價于 Just (+)。接下來由于 partial application 的關(guān)系,Just (+) <*> Just 3 等價于 Just (3+)。把一個 3 喂給 + 形成另一個只接受一個參數(shù)的函數(shù),他的效果等于加上 3。最后 Just (3+) <*> Just 5 被運算,其結(jié)果是 Just 8

這樣很棒吧!用 applicative style 的方式來使用 applicative functors。像是 pure f <*> x <*> y <*> ... 就讓我們可以拿一個接受多個參數(shù)的函數(shù),而且這些參數(shù)不一定是被包在 functor 中。就這樣來套用在多個在 functor context 的值。這個函數(shù)可以吃任意多的參數(shù),畢竟 <*> 只是做 partial application 而已。

如果我們考慮到 pure f <*> x 等于 fmap f x 的話,這樣的用法就更方便了。這是 applicative laws 的其中一條。我們稍后會更仔細(xì)地檢視這條定律?,F(xiàn)在我們先依直覺來使用他。就像我們先前所說的,pure 把一個值放進一個預(yù)設(shè)的 context 中。如果我們要把一個函數(shù)放在一個預(yù)設(shè)的 context,然后把他取出并套用在放在另一個 applicative functor 的值。我們會做的事就是把函數(shù) map over 那個 applicative functor。但我們不會寫成 pure f <*> x <*> y <*> ...,而是寫成 fmap f x <*> y <*> ...。這也是為什么 Control.Applicative 會 export 一個函數(shù) <$>,他基本上就是中綴版的 fmap。他是這么被定義的:

(<$>) :: (Functor f) => (a -> b) -> f a -> f b  
f <$> x = fmap f x  
要記住型別變數(shù)跟參數(shù)的名字還有值綁定的名稱不沖突。``f`` 在函數(shù)的型別宣告中是型別變數(shù),說明 ``f`` 應(yīng)該要滿足 ``Functor`` typeclass 的條件。而在函數(shù)本體中的 ``f`` 則表示一個函數(shù),我們將他 map over x。我們同樣用 ``f`` 來表示他們并代表他們是相同的東西。

<$> 的使用顯示了 applicative style 的好處。如果我們想要將 f 套用三個 applicative functor。我們可以寫成 f <$> x <*> y <*> z。如果參數(shù)不是 applicative functor 而是普通值的話。我們則寫成 f x y z。

我們再仔細(xì)看看他是如何運作的。我們有一個 Just "johntra"Just "volta" 這樣的值,我們希望將他們結(jié)合成一個 String,并且包含在 Maybe 中。我們會這樣做:

ghci> (++) <$> Just "johntra" <*> Just "volta"  
Just "johntravolta"  

可以將上面的跟下面這行比較一下:

ghci> (++) "johntra" "volta"  
"johntravolta"  

可以將一個普通的函數(shù)套用在 applicative functor 上真不錯。只要稍微寫一些 <$><*> 就可以把函數(shù)變成 applicative style,可以操作 applicatives 并回傳 applicatives。

總之當(dāng)我們在做 (++) <$> Just "johntra" <*> Just "volta" 時,首先我們將 (++) map over 到 Just "johntra",然后產(chǎn)生 Just ("johntra"++),其中 (++) 的型別為 (++) :: [a] -> [a] -> [a]Just ("johntra"++) 的型別為 Maybe ([Char] -> [Char])。注意到 (++) 是如何吃掉第一個參數(shù),以及我們是怎么決定 aChar 的。當(dāng)我們做 Just ("johntra"++) <*> Just "volta",他接受一個包在 Just 中的函數(shù),然后 map over Just "volta",產(chǎn)生了 Just "johntravolta"。如果兩個值中有任意一個為 Nothing,那整個結(jié)果就會是 Nothing

到目前為止我們只有用 Maybe 當(dāng)作我們的案例,你可能也會想說 applicative functor 差不多就等于 Maybe。不過其實有許多其他 Applicative 的 instance。我們來看看有哪些。

List 也是 applicative functor。很驚訝嗎?來看看我們是怎么定義 []Applicative 的 instance 的。

instance Applicative [] where  
    pure x = [x]  
    fs <*> xs = [f x | f <- fs, x <- xs]  

早先我們說過 pure 是把一個值放進預(yù)設(shè)的 context 中。換種說法就是一個會產(chǎn)生那個值的最小 context。而對 list 而言最小 context 就是 [],但由于空的 list 并不包含一個值,所以我們沒辦法把他當(dāng)作 pure。這也是為什么 pure 其實是接受一個值然后回傳一個包含單元素的 list。同樣的,Maybe 的最小 context 是 Nothing,但他其實表示的是沒有值。所以 pure 其實是被實作成 Just 的。

ghci> pure "Hey" :: [String]  
["Hey"]  
ghci> pure "Hey" :: Maybe String  
Just "Hey"  

至于 <*> 呢?如果我們假定 <*> 的型別是限制在 list 上的話,我們會得到 (<*>) :: [a -> b] -> [a] -> [b]。他是用 list comprehension 來實作的。<*> 必須要從左邊的參數(shù)取出函數(shù),將他 map over 右邊的參數(shù)。但左邊的 list 有可能不包含任何函數(shù),也可能包含一個函數(shù),甚至是多個函數(shù)。而右邊的 list 有可能包含多個值。這也是為什么我們用 list comprehension 的方式來從兩個 list 取值。我們要對左右任意的組合都做套用的動作。而得到的結(jié)果就會是左右兩者任意組合的結(jié)果。

ghci> [(*0),(+100),(^2)] <*> [1,2,3]  
[0,0,0,101,102,103,1,4,9]  

左邊的 list 包含三個函數(shù),而右邊的 list 有三個值。所以結(jié)果會是有九個元素的 list。在左邊 list 中的每一個函數(shù)都被套用到右邊的值。如果我們今天在 list 中的函數(shù)是接收兩個參數(shù)的,我們也可以套用到兩個 list 上。

ghci> [(+),(*)] <*> [1,2] <*> [3,4]  
[4,5,5,6,3,4,6,8]  

由于 <*> 是 left-associative,也就是說 [(+),(*)] <*> [1,2] 會先運作,產(chǎn)生 [(1+),(2+),(1*),(2*)]。由于左邊的每一個函數(shù)都套用至右邊的每一個值。也就產(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."] 

看看我們是如何將一個接受兩個字串參數(shù)的函數(shù)套用到兩個 applicative functor 上的,只要用適當(dāng)?shù)?applicative 運算子就可以達(dá)成。

你可以將 list 看作是一個 non-deterministic 的計算。而對于像 100 或是 "what" 這樣的值則是 deterministic 的計算,只會有一個結(jié)果。而 [1,2,3] 則可以看作是沒有確定究竟是哪一種結(jié)果。所以他代表的是所有可能的結(jié)果。當(dāng)你在做 (+) <$> [1,2,3] <*> [4,5,6],你可以想做是把兩個 non-deterministic 的計算做 +,只是他會產(chǎn)生另一個 non-deterministic 的計算,而且結(jié)果更加不確定。

Applicative style 對于 list 而言是一個取代 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]     

我們只是從兩個 list 中取出元素,并將一個函數(shù)套用在任何元素的組合上。這也可以用 applicative style 的方式來寫:

ghci> (*) <$> [2,5,10] <*> [8,10,11]  
[16,20,22,40,50,55,80,100,110]  

這寫法對我來說比較清楚。可以清楚表達(dá)我們是要對兩個 non-deterministic 的計算做 *。如果我們想要所有相乘大于 50 可能的計算結(jié)果,我們會這樣寫:

ghci> filter (>50) $ (*) <$> [2,5,10] <*> [8,10,11]  
[55,80,100,110]  

很容易看到 pure f <*> xs 等價于 fmap f xs。而 pure f 就是 [f],而且 [f] <*> xs 可將左邊的每個函數(shù)套用至右邊的每個值。但左邊其實只有一個函數(shù),所以他做起來就像是 mapping。

另一個我們已經(jīng)看過的 Applicative 的 instance 是 IO,來看看他是怎么實作的:

instance Applicative IO where  
    pure = return  
    a <*> b = do  
        f <- a  
        x <- b  
        return (f x)  

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

由于 pure 是把一個值放進最小的 context 中,所以將 return 定義成 pure 是很合理的。因為 return 也是做同樣的事情。他做了一個不做任何事情的 I/O action,他可以產(chǎn)生某些值來作為結(jié)果,但他實際上并沒有做任何 I/O 的動作,例如說印出結(jié)果到終端或是檔案。

如果 <*> 被限定在 IO 上操作的話,他的型別會是 (<*>) :: IO (a -> b) -> IO a -> IO b。他接受一個產(chǎn)生函數(shù)的 I/O action,還有另一個 I/O action,并從以上兩者創(chuàng)造一個新的 I/O action,也就是把第二個參數(shù)喂給第一個參數(shù)。而得到回傳的結(jié)果,然后放到新的 I/O action 中。我們用 do 的語法來實作他。你還記得的話 do 就是把好幾個 I/O action 黏在一起,變成一個大的 I/O action。

而對于 Maybe[] 而言,我們可以把 <*> 想做是從左邊的參數(shù)取出一個函數(shù),然后套用到右邊的參數(shù)上。至于 IO,這種取出的類比方式仍然適用,但我們必須多加一個 sequencing 的概念,因為我們是從兩個 I/O action 中取值,也是在 sequencing,把他們黏成一個。我們從第一個 I/O action 中取值,但要取出 I/O action 的結(jié)果,他必須要先被執(zhí)行過。

考慮下面這個范例:

myAction :: IO String  
myAction = do  
    a <- getLine  
    b <- getLine  
    return $ a ++ b  

這是一個提示使用者輸入兩行并產(chǎn)生將兩行輸入串接在一起結(jié)果的一個 I/O action。我們先把兩個 getLine 黏在一起,然后用一個 return,這是因為我們想要這個黏成的 I/O action 包含 a ++ b 的結(jié)果。我們也可以用 applicative style 的方式來描述:

myAction :: IO String  
myAction = (++) <$> getLine <*> getLine  

我們先前的作法是將兩個 I/O action 的結(jié)果喂給函數(shù)。還記得 getLine 的型別是 getLine :: IO String。當(dāng)我們對 applicative functor 使用 <*> 的時候,結(jié)果也會是 applicative functor。

如果我們再使用盒子的類比,我們可以把 getLine 想做是一個去真實世界中拿取字串的盒子。而 (++) <$> getLine <*> getLine 會創(chuàng)造一個比較大的盒子,這個大盒子會派兩個盒子去終端拿取字串,并把結(jié)果串接起來放進自己的盒子中。

(++) <$> getLine <*> getLine 的型別是 IO String,他代表這個表達(dá)式式一個再普通不過的 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 的動作,而且在 binding 之后還呼叫一些函數(shù),最后用 return 來將結(jié)果包起來。 那你可以考慮使用 applicative style,這樣可以更簡潔。

另一個 Applicative 的 instance 是 (->) r。雖然他們通常是用在 code golf 的情況,但他們還是十分有趣的例子。所以我們還是來看一下他們是怎么被實作的。

如果你忘記 ``(->) r`` 的意思,回去翻翻前一章節(jié)我們介紹 ``(->) r`` 作為一個 functor 的范例。