Functors, Applicative Functors 與 Monoids
Haskell 的一些特色,像是純粹性,高階函數,algebraic data types,typeclasses,這些讓我們可以從更高的角度來看到 polymorphism 這件事。不像 OOP 當中需要從龐大的型態階層來思考。我們只需要看看手邊的型態的行為,將他們跟適當地 typeclass 對應起來就可以了。像 Int 的行為跟很多東西很像。好比說他可以比較相不相等,可以從大到小排列,也可以將他們一一窮舉出來。
Typeclass 的運用是很隨意的。我們可以定義自己的資料型態,然後描述他可以怎樣被操作,跟 typeclass 關聯起來便定義了他的行為。由於 Haskell 強大的型態系統,這讓我們只要讀函數的型態宣告就可以知道很多資訊。typeclass 可以定義得很抽象很 general。我們之前有看過 typeclass 定義了可以比較兩個東西是否相等,或是定義了可以比較兩個東西的大小。這些是既抽象但又描述簡潔的行為,但我們不會認為他們有什麼特別之處,因為我們時常碰到他們。最近我們看過了 functor,基本上他們是一群可以被 map over 的物件。這是其中一個例子能夠抽象但又漂亮地描述行為。在這一章中,我們會詳加闡述 functors,並會提到比較強一些的版本,也就是 applicative functors。我們也會提到 monoids。

溫習 Functors

我們已經在之前的章節提到 functors。如果你還沒讀那個章節,也許你應該先去看看。或是你直接假裝你已經讀過了。
來快速複習一下:Functors 是可以被 map over 的物件,像是 lists,Maybe,trees 等等。在 Haskell 中我們是用 Functor 這個 typeclass 來描述他。這個 typeclass 只有一個 method,叫做 fmap,他的型態是 fmap :: (a -> b) -> f a -> f b。這型態說明了如果給我一個從 a 映到 b 的函數,以及一個裝了 a 的盒子,我會回給你一個裝了 b 的盒子。就好像用這個函數將每個元素都轉成 b 一樣
1
*給一點建議*。這盒子的比喻嘗試讓你抓到些 functors 是如何運作的感覺。在之後我們也會用相同的比喻來比喻 applicative functors 跟 monads。在多數情況下這種比喻是恰當的,但不要過度引申,有些 functors 是不適用這個比喻的。一個比較正確的形容是 functors 是一個計算語境(computational context)。這個語境可能是這個 computation 可能帶有值,或是有可能會失敗(像 ``Maybe`` 跟 ``Either a``),或是他可能有多個值(像 lists),等等。
Copied!
如果一個 type constructor 要是 Functor 的 instance,那他的 kind 必須是 * -> *,這代表他必須剛好接受一個 type 當作 type parameter。像是 Maybe 可以是 Functor 的一個 instance,因為他接受一個 type parameter,來做成像是 Maybe Int,或是 Maybe String。如果一個 type constructor 接受兩個參數,像是 Either,我們必須給他兩個 type parameter。所以我們不能這樣寫:instance Functor Either where,但我們可以寫 instance Functor (Either a) where,如果我們把 fmap 限縮成只是 Either a 的,那他的型態就是 fmap :: (b -> c) -> Either a b -> Either a c。就像你看到的,Either a 的是固定的一部分,因為 Either a 只恰好接受一個 type parameter,但 Either 則要接受兩個 type parameters。這樣 fmap 的型態變成 fmap :: (b -> c) -> Either b -> Either c,這不太合理。
我們知道有許多型態都是 Functor 的 instance,像是 []MaybeEither a 以及我們自己寫的 Tree。我們也看到了如何用一個函數 map 他們。在這一章節,我們再多舉兩個例子,也就是 IO(->) r
如果一個值的型態是 IO String,他代表的是一個會被計算成 String 結果的 I/O action。我們可以用 do syntax 來把結果綁定到某個名稱。我們之前把 I/O action 比喻做長了腳的盒子,會到真實世界幫我們取一些值回來。我們可以檢視他們取了什麼值,但一旦看過,我們必須要把值放回盒子中。用這個比喻,IO 的行為就像是一個 functor。
我們來看看 IO 是怎麼樣的一個 Functor instance。當我們 fmap 用一個 function 來 map over I/O action 時,我們會想要拿回一個裝著已經用 function 映射過值的 I/O action。
1
instance Functor IO where
2
fmap f action = do
3
result <- action
4
return (f result)
Copied!
對一個 I/O action 做 map over 動作的結果仍會是一個 I/O action,所以我們才用 do syntax 來把兩個 I/O action 黏成一個。在 fmap 的實作中,我們先執行了原本傳進的 I/O action,並把結果綁定成 result。然後我們寫了 return (f result)return 就如你所知道的,是一個只會回傳包了你傳給他東西的 I/O action。還有一個 do block 的回傳值一定是他最後一個 I/O action 的回傳值。這也是為什麼我們需要 return。其實他只是回傳包了 f result 的 I/O action。
我們可以再多實驗一下來找到些感覺。來看看這段 code:
1
main = do line <- getLine
2
let line' = reverse line
3
putStrLn $ "You said " ++ line' ++ " backwards!"
4
putStrLn $ "Yes, you really said" ++ line' ++ " backwards!"
Copied!
這程式要求使用者輸入一行文字,然後印出一行反過來的。 我們可以用 fmap 來改寫:
1
main = do line <- fmap reverse getLine
2
putStrLn $ "You said " ++ line ++ " backwards!"
3
putStrLn $ "Yes, you really said" ++ line ++ " backwards!"
Copied!
就像我們用 fmap reverse 來 map over Just "blah" 會得到 Just "halb",我們也可以 fmap reverse 來 map over getLinegetLine 是一個 I/O action,他的 type 是 IO String,而用 reverse 來 map over 他會回傳一個取回一個字串並 reverse 他的 I/O action。就像我們 apply 一個 function 到一個 Maybe 一樣,我們也可以 apply 一個 function 到一個 IO,只是這個 IO 會跑去外面拿回某些值。然後我們把結果用 <- 綁定到某個名稱,而這個名稱綁定的值是已經 reverse 過了。
fmap (++"!") getLine 這個 I/O action 表現得就像 getLine,只是他的結果多了一個 "!" 在最後。
如果我們限縮 fmapIO 型態上,那 fmap 的型態是 fmap :: (a -> b) -> IO a -> IO bfmap 接受一個函數跟一個 I/O action,並回傳一個 I/O action 包含了已經 apply 過 function 的結果。
如果你曾經注意到你想要將一個 I/O action 綁定到一個名稱上,只是為了要 apply 一個 function。你可以考慮使用 fmap,那會更漂亮地表達這件事。或者你想要對 functor 中的資料做 transformation,你可以先將你要用的 function 寫在 top level,或是把他作成一個 lambda function,甚至用 function composition。
1
import Data.Char
2
import Data.List
3
4
main = do line <- fmap (intersperse '-' . reverse . map toUpper) getLine
5
putStrLn line
Copied!
1
$ runhaskell fmapping_io.hs
2
hello there
3
E-R-E-H-T- -O-L-L-E-H
Copied!
正如你想的,intersperse '-' . reverse . map toUpper 合成了一個 function,他接受一個字串,將他轉成大寫,然後反過來,再用 intersperse '-' 安插'-'。他是比較漂亮版本的 (\xs -> intersperse '-' (reverse (map toUpper xs)))
另一個 Functor 的案例是 (->) r,只是我們先前沒有注意到。你可能會困惑到底 (->) r 究竟代表什麼?一個 r -> a 的型態可以寫成 (->) r a,就像是 2 + 3 可以寫成 (+) 2 3 一樣。我們可以從一個不同的角度來看待 (->) r a,他其實只是一個接受兩個參數的 type constructor,好比 Either。但記住我們說過 Functor 只能接受一個 type constructor。這也是為什麼 (->) 不是 Functor 的一個 instance,但 (->) r 則是。如果程式的語法允許的話,你也可以將 (->) r 寫成 (r ->)。就如 (2+) 代表的其實是 (+) 2。至於細節是如何呢?我們可以看看 Control.Monad.Instances
1
我們通常說一個接受任何東西以及回傳隨便一個東西的函數型態是 ``a -> b``。``r -> a`` 是同樣意思,只是把符號代換了一下。
Copied!
1
instance Functor ((->) r) where
2
fmap f g = (\x -> f (g x))
Copied!
如果語法允許的話,他可以被寫成
1
instance Functor (r ->) where
2
fmap f g = (\x -> f (g x))
Copied!
但其實是不允許的,所以我們必須寫成第一種的樣子。
首先我們來看看 fmap 的型態。他的型態是 fmap :: (a -> b) -> f a -> f b。我們把所有的 f 在心裡代換成 (->) r。則 fmap 的型態就變成 fmap :: (a -> b) -> ((->) r a) -> ((->) r b)。接著我們把 (->) r a(->) r b 換成 r -> ar -> b。則我們得到 fmap :: (a -> b) -> (r -> a) -> (r -> b)
從上面的結果看到將一個 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 在做的事。如果你再仔細看看 instance 的定義,會發現真的就是一個 function composition。
1
instance Functor ((->) r) where
2
fmap = (.)
Copied!
這很明顯就是把 fmap 當 composition 在用。可以用 :m + Control.Monad.Instances 把模組裝載進來,並做一些嘗試。
1
ghci> :t fmap (*3) (+100)
2
fmap (*3) (+100) :: (Num a) => a -> a
3
ghci> fmap (*3) (+100) 1
4
303
5
ghci> (*3) `fmap` (+100) $ 1
6
303
7
ghci> (*3) . (+100) $ 1
8
303
9
ghci> fmap (show . (*3)) (*100) 1
10
"300"
Copied!
我們呼叫 fmap 的方式是 infix 的方式,這跟 . 很像。在第二行,我們把 (*3) map over 到 (+100) 上,這會回傳一個先把輸入值 (+100)(*3) 的 function,我們再用 1 去呼叫他。
到這邊為止盒子的比喻還適用嗎?如果你硬是要解釋的話還是解釋得通。當我們將 fmap (+3) map over Just 3 的時候,對於 Maybe 我們很容易把他想成是裝了值的盒子,我們只是對盒子裡面的值 (+3)。但對於 fmap (*3) (+100) 呢?你可以把 (+100) 想成是一個裝了值的盒子。有點像把 I/O action 想成長了腳的盒子一樣。對 (+100) 使用 fmap (*3) 會產生另一個表現得像 (+100) 的 function。只是在算出值之前,會再多計算 (*3)。這樣我們可以看出來 fmap 表現得就像 . 一樣。
fmap 等同於 function composition 這件事對我們來說並不是很實用,但至少是一個有趣的觀點。這也讓我們打開視野,看到盒子的比喻不是那麼恰當,functors 其實比較像 computation。function 被 map over 到一個 computation 會產生經由那個 function 映射過後的 computation。
在我們繼續看 fmap 該遵守的規則之前,我們再看一次 fmap 的型態,他是 fmap :: (a -> b) -> f a -> f b。很明顯我們是在討論 Functor,所以為了簡潔,我們就不寫 (Functor f) => 的部份。當我們在學 curry 的時候,我們說過 Haskell 的 function 實際上只接受一個參數。一個型態是 a -> b -> c 的函數實際上是接受 a 然後回傳 b -> c,而 b -> c 實際上接受一個 b 然後回傳一個 c。如果我們用比較少的參數呼叫一個函數,他就會回傳一個函數需要接受剩下的參數。所以 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 來做的實驗。
1
ghci> :t fmap (*2)
2
fmap (*2) :: (Num a, Functor f) => f a -> f a
3
ghci> :t fmap (replicate 3)
4
fmap (replicate 3) :: (Functor f) => f a -> f [a]
Copied!
fmap (*2) 接受一個 functor f,並回傳一個基於數字的 functor。那個 functor 可以是 list,可以是 Maybe,可以是 Either Stringfmap (replicate 3) 可以接受一個基於任何型態的 functor,並回傳一個基於 list 的 functor。
1
當我們提到 functor over numbers 的時候,你可以想像他是一個 functor 包含有許多數字在裡面。前面一種說法其實比較正確,但後面一種說法比較容易讓人理解。
Copied!
這樣的觀察在我們只有綁定一個部份套用的函數,像是 fmap (++"!"),的時候會顯得更清楚,
你可以把 fmap 想做是一個函數,他接受另一個函數跟一個 functor,然後把函數對 functor 每一個元素做映射,或你可以想做他是一個函數,他接受一個函數並把他 lift 到可以在 functors 上面操作。兩種想法都是正確的,而且在 Haskell 中是等價。
fmap (replicate 3) :: (Functor f) => f a -> f [a] 這樣的型態代表這個函數可以運作在任何 functor 上。至於確切的行為則要看究竟我們操作的是什麼樣的 functor。如果我們是用 fmap (replicate 3) 對一個 list 操作,那我們會選擇 fmap 針對 list 的實作,也就是只是一個 map。如果我們是碰到 Maybe a。那他在碰到 Just 型態的時候,會對裡面的值套用 replicate 3。而碰到 Nothing 的時候就回傳 Nothing
1
ghci> fmap (replicate 3) [1,2,3,4]
2
[[1,1,1],[2,2,2],[3,3,3],[4,4,4]]
3
ghci> fmap (replicate 3) (Just 4)
4
Just [4,4,4]
5
ghci> fmap (replicate 3) (Right "blah")
6
Right ["blah","blah","blah"]
7
ghci> fmap (replicate 3) Nothing
8
Nothing
9
ghci> fmap (replicate 3) (Left "foo")
10
Left "foo"
Copied!
接下來我們來看看 functor laws。一個東西要成為 functor,必須要遵守某些定律。不管任何一個 functor 都被要求具有某些性質。他們必須是能被 map over 的。對他們呼叫 fmap 應該是要用一個函數 map 每一個元素,不多做任何事情。這些行為都被 functor laws 所描述。對於 Functor 的 instance 來說,總共兩條定律應該被遵守。不過他們不會在 Haskell 中自動被檢查,所以你必須自己確認這些條件。
functor law 的第一條說明,如果我們對 functor 做 map id,那得到的新的 functor 應該要跟原來的一樣。如果寫得正式一點,他代表 fmap id = id。基本上他就是說對 functor 呼叫 fmap id,應該等同於對 functor 呼叫 id 一樣。畢竟 id 只是 identity function,他只會把參數照原樣丟出。他也可以被寫成 \x -> x。如果我們對 functor 的概念就是可以被 map over 的物件,那 fmap id = id 的性就顯而易見。
我們來看看這個定律的幾個案例:
1
ghci> fmap id (Just 3)
2
Just 3
3
ghci> id (Just 3)
4
Just 3
5
ghci> fmap id [1..5]
6
[1,2,3,4,5]
7
ghci> id [1..5]
8
[1,2,3,4,5]
9
ghci> fmap id []
10
[]
11
ghci> fmap id Nothing
12
Nothing
Copied!
如果我們看看 Maybefmap 的實作,我們不難發現第一定律為何被遵守。
1
instance Functor Maybe where
2
fmap f (Just x) = Just (f x)
3
fmap f Nothing = Nothing
Copied!
我們可以想像在 f 的位置擺上 id。我們看到 fmap id 拿到 Just x 的時候,結果只不過是 Just (id x),而 id 有只回傳他拿到的東西,所以可以知道 Just (id x) 等價於 Just x。所以說我們可以知道對 Maybe 中的 Justid 去做 map over 的動作,會拿回一樣的值。
而將 id map over Nothing 會拿回 Nothing 並不稀奇。所以從這兩個 fmap 的實作,我們可以看到的確 fmap id = id 有被遵守。
第二定律描述說先將兩個函數合成並將結果 map over 一個 functor 的結果,應該跟先將第一個函數 map over 一個 functor,再將第二個函數 map over 那個 functor 的結果是一樣的。正式地寫下來的話就是 fmap (f . g) = fmap f . fmap g。或是用另外一種寫法,對於任何一個 functor F,下面這個式子應該要被遵守:fmap (f . g) F = fmap f (fmap g F)
如果我們能夠證明某個型別遵守兩個定律,那我們就可以保證他跟其他 functor 對於映射方面都擁有相同的性質。我們知道如果對他用 fmap,我們知道不會有除了 mapping 以外的事會發生,而他就僅僅會表現成某個可以被 map over 的東西。也就是一個 functor。你可以再仔細檢視 fmap 對於某些型別的實作來了解第二定律。正如我們先前對 Maybe 檢視第一定律一般。
如果你需要的話,我們能在這邊演練一下 Maybe 是如何遵守第二定律的。首先 fmap (f . g) 來 map over Nothing 的話,我們會得到 Nothing。因為用任何函數來 fmap Nothing 的話都會回傳 Nothing。如果我們 fmap f (fmap g Nothing),我們會得到 Nothing。可以看到當面對 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))
如果你不太理解這邊的說明,別擔心。只要確定你了解什麼是函數合成就好。在多數的情況下你可以直覺地對應到這些型別表現得就像 containers 或函數一樣。或是也可以換種方法,只要多嘗試對型別中不同的值做操作你就可以看看型別是否有遵守定律。
我們來看一些經典的例子。這些型別建構子雖然是 Functor 的 instance,但實際上他們並不是 functor,因為他們並不遵守這些定律。我們來看看其中一個型別。
1
data CMaybe a = CNothing | CJust Int a deriving (Show)
Copied!
C 這邊代表的是計數器。他是一種看起來像是 Maybe a 的型別,只差在 Just 包含了兩個 field 而不是一個。在 CJust 中的第一個 field 是 Int,他是扮演計數器用的。而第二個 field 則為型別 a,他是從型別參數來的,而他確切的型別當然會依據我們選定的 CMaybe a 而定。我們來對他作些操作來獲得些操作上的直覺吧。
1
ghci> CNothing
2
CNothing
3
ghci> CJust 0 "haha"
4
CJust 0 "haha"
5
ghci> :t CNothing
6
CNothing :: CMaybe a
7
ghci> :t CJust 0 "haha"
8
CJust 0 "haha" :: CMaybe [Char]
9
ghci> CJust 100 [1,2,3]
10
CJust 100 [1,2,3]
Copied!
如果我們使用 CNothing,就代表不含有 field。如果我們用的是 CJust,那第一個 field 是整數,而第二個 field 可以為任何型別。我們來定義一個 Functor 的 instance,這樣每次我們使用 fmap 的時候,函數會被套用在第二個 field,而第一個 field 會被加一。
1
instance Functor CMaybe where
2
fmap f CNothing = CNothing
3
fmap f (CJust counter x) = CJust (counter+1) (f x)
Copied!
這種定義方式有點像是 Maybe 的定義方式,只差在當我們使用 fmap 的時候,如果碰到的不是空值,那我們不只會套用函數,還會把計數器加一。我們可以來看一些範例操作。
1
ghci> fmap (++"ha") (CJust 0 "ho")
2
CJust 1 "hoha"
3
ghci> fmap (++"he") (fmap (++"ha") (CJust 0 "ho"))
4
CJust 2 "hohahe"
5
ghci> fmap (++"blah") CNothing
6
CNothing
Copied!
這些會遵守 functor laws 嗎?要知道有不遵守的情形,只要找到一個反例就好了。
1
ghci> fmap id (CJust 0 "haha")
2
CJust 1 "haha"
3
ghci> id (CJust 0 "haha")
4
CJust 0 "haha"
Copied!
我們知道 functor law 的第一定律描述當我們用 id 來 map over 一個 functor 的時候,他的結果應該跟只對 functor 呼叫 id 的結果一樣。但我們可以看到這個例子中,這對於 CMaybe 並不遵守。儘管他的確是 Functor typeclass 的一個 instace。但他並不遵守 functor law 因此不是一個 functor。如果有人使用我們的 CMaybe 型別,把他當作 functor 用,那他就會期待 functor laws 會被遵守。但 CMaybe 並沒辦法滿足,便會造成錯誤的程式。當我們使用一個 functor 的時候,函數合成跟 map over 的先後順序不應該有影響。但對於 CMaybe 他是有影響的,因為他紀錄了被 map over 的次數。如果我們希望 CMaybe 遵守 functor law,我們必須要讓 Int 欄位在做 fmap 的時候維持不變。
乍看之下 functor laws 看起來不是很必要,也容易讓人搞不懂,但我們知道如果一個型別遵守 functor laws,那我們就能對他作些基本的假設。如果遵守了 functor laws,我們知道對他做 fmap 不會做多餘的事情,只是用一個函數做映射而已。這讓寫出來的程式碼足夠抽象也容易擴展。因為我們可以用定律來推論型別的行為。
所有在標準函式庫中的 Functor 的 instance 都遵守這些定律,但你可以自己檢查一遍。下一次你定義一個型別為 Functor 的 instance 的時候,花點時間確認他確實遵守 functor laws。一旦你操作過足夠多的 functors 時,你就會獲得直覺,知道他們會有什麼樣的性質跟行為。而且 functor laws 也會覺得顯而易見。但就算沒有這些直覺,你仍然可以一行一行地來找看看有沒有反例讓這些定律失效。
我們可以把 functor 看作輸出具有 context 的值。例如說 Just 3 就是輸出 3,但他又帶有一個可能沒有值的 context。[1,2,3] 輸出三個值,1,23,同時也帶有可能有多個值或沒有值的 context。(+3) 則會帶有一個依賴於參數的 context。
如果你把 functor 想做是輸出值這件事,那你可以把 map over 一個 functor 這件事想成在 functor 輸出的後面再多加一層轉換。當我們做 fmap (+3) [1,2,3] 的時候,我們是把 (+3) 接到 [1,2,3] 後面,所以當我們檢視任何一個 list 的輸出的時候,(+3) 也會被套用在上面。另一個例子是對函數做 map over。當我們做 fmap (+3) (*3),我們是把 (+3) 這個轉換套用在 (*3) 後面。這樣想的話會很自然就會把 fmap 跟函數合成關聯起來(fmap (+3) (*3) 等價於 (+3) . (*3),也等價於 \x -> ((x*3)+3)),畢竟我們是接受一個函數 (*3) 然後套用 (+3) 轉換。最後的結果仍然是一個函數,只是當我們餵給他一個數字的時候,他會先乘上三然後做轉換加上三。這基本上就是函數合成在做的事。

Applicative functors

在這個章節中,我們會學到 applicative functors,也就是加強版的 functors,在 Haskell 中是用在 Control.Applicative 中的 Applicative 這個 typeclass 來定義的。
你還記得 Haskell 中函數預設就是 Curried 的,那代表接受多個參數的函數實際上是接受一個參數然後回傳一個接受剩餘參數的函數,以此類推。如果一個函數的型別是 a -> b -> c,我們通常會說這個函數接受兩個參數並回傳 c,但他實際上是接受 a 並回傳一個 b -> c 的函數。這也是為什麼我們可以用 (f x) y 的方式呼叫 f x y。這個機制讓我們可以 partially apply 一個函數,可以用比較少的參數呼叫他們。可以做成一個函數再餵給其他函數。
到目前為止,當我們要對 functor map over 一個函數的時候,我們用的函數都是只接受一個參數的。但如果我們要 map 一個接受兩個參數的函數呢?我們來看幾個具體的例子。如果我們有 Just 3 然後我們做 fmap (*) (Just 3),那我們會獲得什麼樣的結果?從 MaybeFunctor 的 instance 實作來看,我們知道如果他是 Just something,他會對在 Just 中的 something 做映射。因此當 fmap (*) (Just 3) 會得到 Just ((*) 3),也可以寫做 Just (* 3)。我們得到了一個包在 Just 中的函數。
1
ghci> :t fmap (++) (Just "hey")
2
fmap (++) (Just "hey") :: Maybe ([Char] -> [Char])
3
ghci> :t fmap compare (Just 'a')
4
fmap compare (Just 'a') :: Maybe (Char -> Ordering)
5
ghci> :t fmap compare "A LIST OF CHARS"
6
fmap compare "A LIST OF CHARS" :: [Char -> Ordering]
7
ghci> :t fmap (\x y z -> x + y / z) [3,4,5,6]
8
fmap (\x y z -> x + y / z) [3,4,5,6] :: (Fractional a) => [a -> a -> a]
Copied!
如果我們 map compare 到一個包含許多字元的 list 呢?他的型別是 (Ord a) => a -> a -> Ordering,我們會得到包含許多 Char -> Ordering 型別函數的 list,因為 compare 被 partially apply 到 list 中的字元。他不是包含許多 (Ord a) => a -> Ordering 的函數,因為第一個 a 碰到的型別是 Char,所以第二個 a 也必須是 Char
我們看到如何用一個多參數的函數來 map functor,我們會得到一個包含了函數的 functor。那現在我們能對這個包含了函數的 functor 做什麼呢?我們能用一個吃這些函數的函數來 map over 這個 functor,這些在 functor 中的函數都會被當作參數丟給我們的函數。
1
ghci> let a = fmap (*) [1,2,3,4]
2
ghci> :t a
3
a :: [Integer -> Integer]
4
ghci> fmap (\f -> f 9) a
5
[9,18,27,36]
Copied!
但如果我們的有一個 functor 裡面是 Just (3 *) 還有另一個 functor 裡面是 Just 5,但我們想要把第一個 Just (3 *) map over Just 5 呢?如果是普通的 functor,那就沒救了。因為他們只允許 map 一個普通的函數。即使我們用 \f -> f 9 來 map 一個裝了很多函數的 functor,我們也是使用了普通的函數。我們是無法單純用 fmap 來把包在一個 functor 的函數 map 另一個包在 functor 中的值。我們能用模式匹配 Just 來把函數從裡面抽出來,然後再 map Just 5,但我們是希望有一個一般化的作法,對任何 functor 都有效。
我們來看看 Applicative 這個 typeclass。他位在 Control.Applicative 中,在其中定義了兩個函數 pure<*>。他並沒有提供預設的實作,如果我們想使用他必須要為他們 applicative functor 的實作。typeclass 定義如下:
1
class (Functor f) => Applicative f where
2
pure :: a -> f a
3
(<*>) :: f (a -> b) -> f a -> f b
Copied!
這簡簡單單的三行可以讓我們學到不少。首先來看第一行。他開啟了 Applicative 的定義,並加上 class contraint。描述了一個型別構造子要是 Applicative,他必須也是 Functor。這就是為什麼我們說一個型別構造子屬於 Applicative 的話,他也會是 Functor,因此我們能對他使用 fmap
第一個定義的是 pure。他的型別宣告是 pure :: a -> f af 代表 applicative functor 的 instance。由於 Haskell 有一個優秀的型別系統,其中函數又是將一些參數映射成結果,我們可以從型別宣告中讀出許多訊息。pure 應該要接受一個值,然後回傳一個包含那個值的 applicative functor。我們這邊是用盒子來作比喻,即使有一些比喻不完全符合現實的情況。儘管這樣,a -> f a 仍有許多豐富的資訊,他確實告訴我們他會接受一個值並回傳一個 applicative functor,裡面裝有結果。
對於 pure 比較好的說法是把一個普通值放到一個預設的 context 下,一個最小的 context 但仍然包含這個值。
<*> 也非常有趣。他的型別是 f (a -> b) -> f a -> f b。這有讓你聯想到什麼嗎?沒錯!就是 fmap :: (a -> b) -> f a -> f b。他有點像加強版的 fmap。然而 fmap 接受一個函數跟一個 functor,然後套用 functor 之中的函數。<*> 則是接受一個裝有函數的 functor 跟另一個 functor,然後取出第一個 functor 中的函數將他對第二個 functor 中的值做 map。
我們來看看 MaybeApplicative 實作:
1
instance Applicative Maybe where
2
pure = Just
3
Nothing <*> _ = Nothing
4
(Just f) <*> something = fmap f something
Copied!
從 class 的定義我們可以看到 f 作為 applicative functor 會接受一個具體型別當作參數,所以我們是寫成 instance Applicative Maybe where 而不是寫成 instance Applicative (Maybe a) where
首先看到 pure。他只不過是接受一個東西然後包成 applicative functor。我們寫成 pure = Just 是因為 Just 不過就是一個普通函數。我們其實也可以寫成 pure x = Just x
接著我們定義了 <*>。我們無法從 Nothing 中抽出一個函數,因為 Nothing 並不包含一個函數。所以我們說如果我們要嘗試從 Nothing 中取出一個函數,結果必定是 Nothing。如果你看看 Applicative 的定義,你會看到他有 Functor 的限制,他代表 <*> 的兩個參數都會是 functors。如果第一個參數不是 Nothing,而是一個裝了函數的 Just,而且我們希望將這個函數對第二個參數做 map。這個也考慮到第二個參數是 Nothing 的情況,因為 fmap 任何一個函數至 Nothing 會回傳 Nothing
對於 Maybe 而言,如果左邊是 Just,那 <*> 會從其中抽出了一個函數來 map 右邊的值。如果有任何一個參數是 Nothing。那結果便是 Nothing
來試試看吧!
1
ghci> Just (+3) <*> Just 9
2
Just 12
3
ghci> pure (+3) <*> Just 10
4
Just 13
5
ghci> pure (+3) <*> Just 9
6
Just 12
7
ghci> Just (++"hahah") <*> Nothing
8
Nothing
9
ghci> Nothing <*> Just "woot"
10
Nothing
Copied!
我們看到 pure (+3)Just (+3) 在這個 case 下是一樣的。如果你是在 applicative context 底下跟 Maybe 打交道的話請用 pure,要不然就用 Just。前四個輸入展示了函數是如何被取出並做 map 的動作,但在這個 case 底下,他們同樣也可以用 unwrap 函數來 map over functors。最後一行比較有趣,因為我們試著從 Nothing 取出函數並將他 map 到某個值。結果當然是 Nothing
對於普通的 functors,你可以用一個函數 map over 一個 functors,但你可能沒辦法拿到結果。而 applicative functors 則讓你可以用單一一個函數操作好幾個 functors。看看下面一段程式碼:
1
ghci> pure (+) <*> Just 3 <*> Just 5
2
Just 8
3
ghci> pure (+) <*> Just 3 <*> Nothing
4
Nothing
5
ghci> pure (+) <*> Nothing <*> Just 5
6
Nothing
Copied!
究竟我們寫了些什麽?我們來一步步看一下。<*> 是 left-associative,也就是說 pure (+) <*> Just 3 <*> Just 5 可以寫成 (pure (+) <*> Just 3) <*> Just 5。首先 + 是擺在一個 functor 中,在這邊剛好他是一個 Maybe。所以首先,我們有 pure (+),他等價於 Just (+)。接下來由於 partial application 的關係,Just (+) <*> Just 3 等價於 Just (3+)。把一個 3 餵給 + 形成另一個只接受一個參數的函數,他的效果等於加上 3。最後 Just (3+) <*> Just 5 被運算,其結果是 Just 8
這樣很棒吧!用 applicative style 的方式來使用 applicative functors。像是 pure f <*> x <*> y <*> ... 就讓我們可以拿一個接受多個參數的函數,而且這些參數不一定是被包在 functor 中。就這樣來套用在多個在 functor context 的值。這個函數可以吃任意多的參數,畢竟 <*> 只是做 partial application 而已。
如果我們考慮到 pure f <*> x 等於 fmap f x 的話,這樣的用法就更方便了。這是 applicative laws 的其中一條。我們稍後會更仔細地檢視這條定律。現在我們先依直覺來使用他。就像我們先前所說的,pure 把一個值放進一個預設的 context 中。如果我們要把一個函數放在一個預設的 context,然後把他取出並套用在放在另一個 applicative functor 的值。我們會做的事就是把函數 map over 那個 applicative functor。但我們不會寫成 pure f <*> x <*> y <*> ...,而是寫成 fmap f x <*> y <*> ...。這也是為什麽 Control.Applicative 會 export 一個函數 <gt;,他基本上就是中綴版的 fmap。他是這麼被定義的:
1
(<gt;) :: (Functor f) => (a -> b) -> f a -> f b
2
f <gt; x = fmap f x
Copied!
1
要記住型別變數跟參數的名字還有值綁定的名稱不衝突。``f`` 在函數的型別宣告中是型別變數,說明 ``f`` 應該要滿足 ``Functor`` typeclass 的條件。而在函數本體中的 ``f`` 則表示一個函數,我們將他 map over x。我們同樣用 ``f`` 來表示他們並代表他們是相同的東西。
Copied!
<gt; 的使用顯示了 applicative style 的好處。如果我們想要將 f 套用三個 applicative functor。我們可以寫成 f <gt; x <*> y <*> z。如果參數不是 applicative functor 而是普通值的話。我們則寫成 f x y z
我們再仔細看看他是如何運作的。我們有一個 Just "johntra"Just "volta" 這樣的值,我們希望將他們結合成一個 String,並且包含在 Maybe 中。我們會這樣做:
1
ghci> (++) <gt; Just "johntra" <*> Just "volta"
2
Just "johntravolta"
Copied!
可以將上面的跟下面這行比較一下:
1
ghci> (++) "johntra" "volta"
2
"johntravolta"
Copied!
可以將一個普通的函數套用在 applicative functor 上真不錯。只要稍微寫一些 <gt;<*> 就可以把函數變成 applicative style,可以操作 applicatives 並回傳 applicatives。
總之當我們在做 (++) <gt; Just "johntra" <*> Just "volta" 時,首先我們將 (++) map over 到 Just "johntra",然後產生 Just ("johntra"++),其中 (++) 的型別為 (++) :: [a] -> [a] -> [a]Just ("johntra"++) 的型別為 Maybe ([Char] -> [Char])。注意到 (++) 是如何吃掉第一個參數,以及我們是怎麼決定 aChar 的。當我們做 Just ("johntra"++) <*> Just "volta",他接受一個包在 Just 中的函數,然後 map over Just "volta",產生了 Just "johntravolta"。如果兩個值中有任意一個為 Nothing,那整個結果就會是 Nothing
到目前為止我們只有用 Maybe 當作我們的案例,你可能也會想說 applicative functor 差不多就等於 Maybe。不過其實有許多其他 Applicative 的 instance。我們來看看有哪些。
List 也是 applicative functor。很驚訝嗎?來看看我們是怎麼定義 []Applicative 的 instance 的。
1
instance Applicative [] where
2
pure x = [x]
3
fs <*> xs = [f x | f <- fs, x <- xs]
Copied!
早先我們說過 pure 是把一個值放進預設的 context 中。換種說法就是一個會產生那個值的最小 context。而對 list 而言最小 context 就是 [],但由於空的 list 並不包含一個值,所以我們沒辦法把他當作 pure。這也是為什麼 pure 其實是接受一個值然後回傳一個包含單元素的 list。同樣的,Maybe 的最小 context 是 Nothing,但他其實表示的是沒有值。所以 pure 其實是被實作成 Just 的。
1
ghci> pure "Hey" :: [String]
2
["Hey"]
3
ghci> pure "Hey" :: Maybe String
4
Just "Hey"
Copied!
至於 <*> 呢?如果我們假定 <*> 的型別是限制在 list 上的話,我們會得到 (<*>) :: [a -> b] -> [a] -> [b]。他是用 list comprehension 來實作的。<*> 必須要從左邊的參數取出函數,將他 map over 右邊的參數。但左邊的 list 有可能不包含任何函數,也可能包含一個函數,甚至是多個函數。而右邊的 list 有可能包含多個值。這也是為什麼我們用 list comprehension 的方式來從兩個 list 取值。我們要對左右任意的組合都做套用的動作。而得到的結果就會是左右兩者任意組合的結果。
1
ghci> [(*0),(+100),(^2)] <*> [1,2,3]
2
[0,0,0,101,102,103,1,4,9]
Copied!
左邊的 list 包含三個函數,而右邊的 list 有三個值。所以結果會是有九個元素的 list。在左邊 list 中的每一個函數都被套用到右邊的值。如果我們今天在 list 中的函數是接收兩個參數的,我們也可以套用到兩個 list 上。
1
ghci> [(+),(*)] <*> [1,2] <*> [3,4]
2
[4,5,5,6,3,4,6,8]
Copied!
由於 <*> 是 left-associative,也就是說 [(+),(*)] <*> [1,2] 會先運作,產生 [(1+),(2+),(1*),(2*)]。由於左邊的每一個函數都套用至右邊的每一個值。也就產生 [(1+),(2+),(1*),(2*)] <*> [3,4],其便是最終結果。
list 的 applicative style 是相當有趣的:
1
ghci> (++) <gt; ["ha","heh","hmm"] <*> ["?","!","."]
2
["ha?","ha!","ha.","heh?","heh!","heh.","hmm?","hmm!","hmm."]
Copied!
看看我們是如何將一個接受兩個字串參數的函數套用到兩個 applicative functor 上的,只要用適當的 applicative 運算子就可以達成。
你可以將 list 看作是一個 non-deterministic 的計算。而對於像 100 或是 "what" 這樣的值則是 deterministic 的計算,只會有一個結果。而 [1,2,3] 則可以看作是沒有確定究竟是哪一種結果。所以他代表的是所有可能的結果。當你在做 (+) <gt; [1,2,3] <*> [4,5,6],你可以想做是把兩個 non-deterministic 的計算做 +,只是他會產生另一個 non-deterministic 的計算,而且結果更加不確定。
Applicative style 對於 list 而言是一個取代 list comprehension 的好方式。在第二章中,我們想要看到 [2,5,10][8,10,11] 相乘的結果,所以我們這樣做:
1
ghci> [ x*y | x <- [2,5,10], y <- [8,10,11]]
2
[16,20,22,40,50,55,80,100,110]
Copied!
我們只是從兩個 list 中取出元素,並將一個函數套用在任何元素的組合上。這也可以用 applicative style 的方式來寫:
1
ghci> (*) <gt; [2,5,10] <*> [8,10,11]
2
[16,20,22,40,50,55,80,100,110]
Copied!
這寫法對我來說比較清楚。可以清楚表達我們是要對兩個 non-deterministic 的計算做 *。如果我們想要所有相乘大於 50 可能的計算結果,我們會這樣寫:
1
ghci> filter (>50) $ (*) <gt; [2,5,10] <*> [8,10,11]
2
[55,80,100,110]
Copied!
很容易看到 pure f <*> xs 等價於 fmap f xs。而 pure f 就是 [f],而且 [f] <*> xs 可將左邊的每個函數套用至右邊的每個值。但左邊其實只有一個函數,所以他做起來就像是 mapping。
另一個我們已經看過的 Applicative 的 instance 是 IO,來看看他是怎麼實作的:
1
instance Applicative IO where
2
pure = return
3
a <*> b = do
4
f <- a
5
x <- b
6
return (f x)
Copied!
由於 pure 是把一個值放進最小的 context 中,所以將 return 定義成 pure 是很合理的。因為 return 也是做同樣的事情。他做了一個不做任何事情的 I/O action,他可以產生某些值來作為結果,但他實際上並沒有做任何 I/O 的動作,例如說印出結果到終端或是檔案。
如果 <*> 被限定在 IO 上操作的話,他的型別會是 (<*>) :: IO (a -> b) -> IO a -> IO b。他接受一個產生函數的 I/O action,還有另一個 I/O action,並從以上兩者創造一個新的 I/O action,也就是把第二個參數餵給第一個參數。而得到回傳的結果,然後放到新的 I/O action 中。我們用 do 的語法來實作他。你還記得的話 do 就是把好幾個 I/O action 黏在一起,變成一個大的 I/O action。
而對於 Maybe[] 而言,我們可以把 <*> 想做是從左邊的參數取出一個函數,然後套用到右邊的參數上。至於 IO,這種取出的類比方式仍然適用,但我們必須多加一個 sequencing 的概念,因為我們是從兩個 I/O action 中取值,也是在 sequencing,把他們黏成一個。我們從第一個 I/O action 中取值,但要取出 I/O action 的結果,他必須要先被執行過。
考慮下面這個範例:
1
myAction :: IO String
2
myAction = do
3
a <- getLine
4
b <- getLine
5
return $ a ++ b
Copied!
這是一個提示使用者輸入兩行並產生將兩行輸入串接在一起結果的一個 I/O action。我們先把兩個 getLine 黏在一起,然後用一個 return,這是因為我們想要這個黏成的 I/O action 包含 a ++ b 的結果。我們也可以用 applicative style 的方式來描述:
1
myAction :: IO String
2
myAction = (++) <gt; getLine <*> getLine
Copied!
我們先前的作法是將兩個 I/O action 的結果餵給函數。還記得 getLine 的型別是 getLine :: IO String。當我們對 applicative functor 使用 <*> 的時候,結果也會是 applicative functor。
如果我們再使用盒子的類比,我們可以把 getLine 想做是一個去真實世界中拿取字串的盒子。而 (++) <gt; getLine <*> getLine 會創造一個比較大的盒子,這個大盒子會派兩個盒子去終端拿取字串,並把結果串接起來放進自己的盒子中。
(++) <gt; getLine <*> getLine 的型別是 IO String,他代表這個表達式式一個再普通不過的 I/O action,他裡面也裝著某種值。這也是為什麼我們可以這樣寫:
1
main = do
2
a <- (++) <gt; getLine <*> getLine
3
putStrLn $ "The two lines concatenated turn out to be: " ++ a
Copied!
如果你發現你是在做 binding I/O action 的動作,而且在 binding 之後還呼叫一些函數,最後用 return 來將結果包起來。 那你可以考慮使用 applicative style,這樣可以更簡潔。
另一個 Applicative 的 instance 是 (->) r。雖然他們通常是用在 code golf 的情況,但他們還是十分有趣的例子。所以我們還是來看一下他們是怎麼被實作的。
1
如果你忘記 ``(->) r`` 的意思,回去翻翻前一章節我們介紹 ``(->) r`` 作為一個 functor 的範例。
Copied!
1
instance Applicative ((->) r) where
2
pure x = (\_ -> x)
3
f <*> g = \x -> f x (g x)
Copied!
當我們用 pure 將一個值包成 applicative functor 的時候,他產生的結果永遠都會是那個值。也就是最小的 context。那也是為什麼對於 function 的 pure 實作來講,他就是接受一個值,然後造一個函數永遠回傳那個值,不管他被餵了什麼參數。如果你限定 pure 的型別至 (->) r 上,他就會是 pure :: a -> (r -> a)
1
ghci> (pure 3) "blah"
2
3
Copied!
由於 currying 的關係,函數套用是 left-associative,所以我們忽略掉括弧。
1
ghci> pure 3 "blah"
2
3
Copied!
<*> 的實作是比較不容易瞭解的,我們最好看一下怎麼用 applicative style 的方式來使用作為 applicative functor 的 function。
1
ghci> :t (+) <gt; (+3) <*> (*100)
2
(+) <gt; (+3) <*> (*100) :: (Num a) => a -> a
3
ghci> (+) <gt; (+3) <*> (*100) $ 5
4
508
Copied!
將兩個 applicative functor 餵給 <*> 可以產生一個新的 applicative functor,所以如果我們丟給他兩個函數,我們能得到一個新的函數。所以是怎麼一回事呢?當我們做 (+) <gt; (+3) <*> (*100),我們是在實作一個函數,他會將 (+3)(*100) 的結果再套用 +。要看一個實際的範例的話,可以看一下 (+) <gt; (+3) <*> (*100) $ 5 首先 5 被丟給 (+3)(*100),產生 8500。然後 + 被套用到 8500,得到 508
1
ghci> (\x y z -> [x,y,z]) <gt; (+3) <*> (*2) <*> (/2) $ 5
2
[8.0,10.0,2.5]
Copied!
這邊也一樣。我們創建了一個函數,他會呼叫 \x y z -> [x,y,z],而丟的參數是 (+3), (*2)(/2)5 被丟給以上三個函數,然後他們結果又接到 \x y z -> [x, y, z]
你可以將函數想做是裝著最終結果的盒子,所以 k <gt; f <*> g 會製造一個函數,他會將 fg 的結果丟給 k。當我們做 (+) <gt; Just 3 <*> Just 5,我們是用 + 套用在一些可能有或可能沒有的值上,所以結果也會是可能有或沒有。當我們做 (+) <gt; (+10) <*> (+5),我們是將 + 套用在 (+10)(+5) 的結果上,而結果也會是一個函數,當被餵給一個參數的時候會產生結果。
我們通常不會將函數當作 applicative 用,不過仍然值得當作練習。對於 (->) r 怎麼定義成 Applicative 的並不是真的那麼重要,所以如果你不是很懂的話也沒關係。這只是讓你獲得一些操作上的直覺罷了。
一個我們之前還沒碰過的 Applicative 的 instance 是 ZipList,他是包含在 Control.Applicative 中。
對於 list 要作為一個 applicative functor 可以有多種方式。我們已經介紹過其中一種。如果套用 <*>,左邊是許多函數,而右邊是許多值,那結果會是函數套用到值的所有組合。如果我們做 [(+3),(*2)] <*> [1,2]。那 (+3) 會先套用至 12。接著 (*2) 套用至 12。而得到 [4,5,2,4]
然而 [(+3),(*2)] <*> [1,2] 也可以這樣運作:把左邊第一個函數套用至右邊第一個值,接著左邊第二個函數套用右邊第二個值,以此類推。這樣得到的會是 [4,4]。或是 [1 + 3, 2 * 2]
由於一個型別不能對同一個 typeclass 定義兩個 instance,所以才會定義了 ZipList a,他只有一個構造子 ZipList,他只包含一個欄位,他的型別是 list。
1
instance Applicative ZipList where
2
pure x = ZipList (repeat x)
3
ZipList fs <*> ZipList xs = ZipList (zipWith (\f x -> f x) fs xs)
Copied!
<*> 做的就是我們之前說的。他將第一個函數套用至第一個值,第二個函數套用第二個值。這也是 zipWith (\f x -> f x) fs xs 做的事。由於 zipWith 的特性,所以結果會跟 list 中比較短的那個一樣長。
pure 也值得我們討論一下。他接受一個值,把他重複地放進一個 list 中。pure "haha" 就會是 ZipList (["haha","haha","haha"...。這可能會造成些混淆,畢竟我們說過 pure 是把一個值放進一個最小的 context 中。而你會想說無限長的 list 不可能會是一個最小的 context。但對於 zip list 來說這是很合理的,因為他必須在 list 的每個位置都有值。這也遵守了 pure f <*> xs 必須要等價於 fmap f xs 的特性。如果 pure 3 只是回傳 ZipList [3],那 pure (*2) <*> ZipList [1,5,10] 就只會算出 ZipList [2],因為兩個 zip list 算出結果的長度會是比較短的那個的長度。如果我們 zip 一個有限長的 list 以及一個無限長的 list,那結果的長會是有限長的 list 的長度。
那 zip list 是怎麼用 applicative style 操作的呢?我們來看看,ZipList a 型別並沒有定義成 Show 的 instance,所以我們必須用 getZipList 函數來從 zip list 取出一個普通的 list。
1
ghci> getZipList $ (+) <gt; ZipList [1,2,3] <*> ZipList [100,100,100]
2
[101,102,103]
3
ghci> getZipList $ (+) <gt; ZipList [1,2,3] <*> ZipList [100,100..]
4
[101,102,103]
5
ghci> getZipList $ max <gt; ZipList [1,2,3,4,5,3] <*> ZipList [5,3,1,2]
6
[5,3,3,4]
7
ghci> getZipList $ (,,) <gt; ZipList "dog" <*> ZipList "cat" <*> ZipList "rat"
8
[('d','c','r'),('o','a','a'),('g','t','t')]
Copied!
1
``(,,)`` 函數跟 ``\x y z -> (x,y,z)`` 是等價的,而 ``(,)`` 跟 ``\x y -> (x,y)`` 是等價的。
Copied!
除了 zipWith,標準函式庫中也有 zipWith3, zipWith4 之類的函數,最多支援到 7。zipWith 接受一個接受兩個參數的函數,並把兩個 list zip 起來。zipWith3 則接受一個接受三個參數的函數,然後把三個 list zip 起來。以此類推。用 applicative style 的方式來操作 zip list 的話,我們就不需要對每個數量的 list 都定義一個獨立的 zip 函數來 zip 他們。我們只需要用 applicative style 的方式來把任意數量的 list zip 起來就可以了。
Control.Applicative 定義了一個函數叫做 liftA2,他的型別是 liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c。他定義如下:
1
liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c
2
liftA2 f a b = f <gt; a <*> b
Copied!
並沒有太難理解的東西,他不過就是對兩個 applicatives 套用函數而已,而不用我們剛剛熟悉的 applicative style。我們提及他的理由只是要展示為什麼 applicative functors 比起一般的普通 functor 要強。如果只是普通的 functor 的話,我們只能將一個函數 map over 這個 functor。但有了 applicative functor,我們可以對好多個 functor 套用一個函數。看看這個函數的型別,他會是 (a -> b -> c) -> (f a -> f b -> f c)。當我們從這樣的角度來看他的話,我們可以說 liftA2 接受一個普通的二元函數,並將他升級成一個函數可以運作在兩個 functor 之上。
另外一個有趣的概念是,我們可以接受兩個 applicative functor 並把他們結合成一個 applicative functor,這個新的將這兩個 applicative functor 裝在 list 中。舉例來說,我們現在有 Just 3Just 4。我們假設後者是一個只包含單元素的 list。
1
ghci> fmap (\x -> [x]) (Just 4)
2
Just [4]
Copied!
所以假設我們有 Just 3Just [4]。我們有怎麼得到 Just [3,4] 呢?很簡單。
1
ghci> liftA2 (:) (Just 3) (Just [4])
2
Just [3,4]
3
ghci> (:) <gt; Just 3 <*> Just [4]
4
Just [3,4]
Copied!
還記得 : 是一個函數,他接受一個元素跟一個 list,並回傳一個新的 list,其中那個元素已經接在前面。現在我們有了 Just [3,4],我們能夠將他跟 Just 2 綁在一起變成 Just [2,3,4] 嗎?當然可以。我們可以將任意數量的 applicative 綁在一起變成一個 applicative,裡面包含一個裝有結果的 list。我們試著實作一個函數,他接受一串裝有 applicative 的 list,然後回傳一個 applicative 裡面有一個裝有結果的 list。我們稱呼他為 sequenceA
1
sequenceA :: (Applicative f) => [f a] -> f [a]
2
sequenceA [] = pure []
3
sequenceA (x:xs) = (:) <gt; x <*> sequenceA xs
Copied!
居然用到了遞迴!首先我們來看一下他的型別。他將一串 applicative 的 list 轉換成一個 applicative 裝有一個 list。從這個資訊我們可以推測出邊界條件。如果我們要將一個空的 list 變成一個裝有 list 的 applicative。我們只要把這個空的 list 放進一個預設的 context。現在來看一下我們怎麼用遞迴的。如果們有一個可以分成頭跟尾的 list(x 是一個 applicative 而 xs 是一串 applicatve),我們可以對尾巴呼叫 sequenceA,便會得到一個裝有 list 的 applicative。然後我們只要將在 x 中的值把他接到裝有 list 的 applicative 前面就可以了。
所以如果我們做 sequenceA [Just 1, Just 2],也就是 (:) <gt; Just 1 <*> sequenceA [Just 2]。那會等價於 (:) <gt; Just 1 <*> ((:) <gt; Just 2 <*> sequenceA [])。我們知道 sequenceA [] 算出來會是 Just [],所以運算式就變成 (:) <gt; Just 1 <*> ((:) <gt; Just 2 <*> Just []),也就是 (:) <gt; Just 1 <*> Just [2],算出來就是 Just [1,2]
另一種實作 sequenceA 的方式是用 fold。要記得幾乎任何需要走遍整個 list 並 accumulate 成一個結果的都可以用 fold 來實作。
1
sequenceA :: (Applicative f) => [f a] -> f [a]
2
sequenceA = foldr (liftA2 (:)) (pure [])
Copied!
我們從右往左走,並且起始的 accumulator 是用 pure []。我們是用 liftA2 (:) 來結合 accumulator 跟 list 中最後的元素,而得到一個 applicative,裡面裝有一個單一元素的一個 list。然後我們再用 liftA2 (:) 來結合 accumulator 跟最後一個元素,直到我們只剩下 accumulator 為止,而得到一個 applicative,裡面裝有所有結果。
我們來試試看套用在不同 applicative 上。
1
ghci> sequenceA [Just 3, Just 2, Just 1]
2
Just [3,2,1]
3
ghci> sequenceA [Just 3, Nothing, Just 1]
4
Nothing
5
ghci> sequenceA [(+3),(+2),(+1)] 3
6
[6,5,4]
7
ghci> sequenceA [[1,2,3],[4,5,6]]
8
[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]
9
ghci> sequenceA [[1,2,3],[4,5,6],[3,4,4],[]]
10
[]
Copied!
很酷吧。當我們套用在 Maybe 上時,sequenceA 創造一個新的 Maybe,他包含了一個 list 裝有所有結果。如果其中一個值是 Nothing,那整個結果就會是 Nothing。如果你有一串 Maybe 型別的值,但你只在乎當結果不包含任何 Nothing 的情況,這樣的特性就很方便。
當套用在函數時,sequenceA 接受裝有一堆函數的 list,並回傳一個回傳 list 的函數。在我們的範例中,我們寫了一個函數,他只接受一個數值作為參數,他會把他套用至 list 中的每一個函數,並回傳一個包含結果的 list。sequenceA [(+3),(+2),(+1)] 3 會將 3 餵給 (+3), (+2)(+1),然後將所有結果裝在一個 list 中。
(+) <gt; (+3) <*> (*2) 會創見一個接受單一參數的一函數,將他同時餵給 (+3)(*2),然後呼叫 + 來將兩者加起來。同樣的道理,sequenceA [(+3),(*2)] 是製造一個接受單一參數的函數,他會將他餵給所有包含在 list 中的函數。但他最後不是呼叫 +,而是呼叫 :pure [] 來把結果接成一個 list,得到最後的結果。
當我們有一串函數,我們想要將相同的輸入都餵給他們並檢視結果的時候,sequenceA 非常好用。例如說,我們手上有一個數值,但不知道他是否滿足一串 predicate。一種實作的方式是像這樣:
1
ghci> map (\f -> f 7) [(>4),(<10),odd]
2
[True,True,True]
3
ghci> and $ map (\f -> f 7) [(>4),(<10