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 是可以被 map over 的物件,像是 lists,
Maybe
,trees 等等。在 Haskell 中我們是用 Functor
這個 typeclass 來描述他。這個 typeclass 只有一個 method,叫做 fmap
,他的型態是 fmap :: (a -> b) -> f a -> f b
。這型態說明了如果給我一個從 a
映到 b
的函數,以及一個裝了 a
的盒子,我會回給你一個裝了 b
的盒子。就好像用這個函數將每個元素都轉成 b
一樣*給一點建議*。這盒子的比喻嘗試讓你抓到些 functors 是如何運作的感覺。在之後我們也會用相同的比喻來比喻 applicative functors 跟 monads。在多數情況下這種比喻是恰當的,但不要過度引申,有些 functors 是不適用這個比喻的。一個比較正確的形容是 functors 是一個計算語境(computational context)。這個語境可能是這個 computation 可能帶有值,或是有可能會失敗(像 ``Maybe`` 跟 ``Either a``),或是他可能有多個值(像 lists),等等。
如果一個 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,像是 []
,Maybe
,Either 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。instance Functor IO where
fmap f action = do
result <- action
return (f result)
對一個 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:
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"
會得到 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
會跑去外面拿回某些值。然後我們把結果用 <-
綁定到某個名稱,而這個名稱綁定的值是已經 reverse
過了。而
fmap (++"!") getLine
這個 I/O action 表現得就像 getLine
,只是他的結果多了一個 "!"
在最後。如果我們限縮
fmap
到 IO
型態上,那 fmap 的型態是 fmap :: (a -> b) -> IO a -> IO b
。fmap
接受一個函數跟一個 I/O action,並回傳一個 I/O action 包含了已經 apply 過 function 的結果。如果你曾經注意到你想要將一個 I/O action 綁定到一個名稱上,只是為了要 apply 一個 function。你可以考慮使用
fmap
,那會更漂亮地表達這件事。或者你想要對 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,他接受一個字串,將他轉成大寫,然後反過來,再用 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
。我們通常說一個接受任何東西以及回傳隨便一個東西的函數型態是 ``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
的型態。他的型態是 fmap :: (a -> b) -> f a -> f b
。我們把所有的 f
在心裡代換成 (->) r
。則 fmap
的型態就變成 fmap :: (a -> b) -> ((->) r a) -> ((->) r b)
。接著我們把 (->) r a
跟 (->) r b
換成 r -> a
跟 r -> 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)
告訴我們什麼?他接受一個從 a
到 b
的 function,跟一個從 r
到 a
的 function,並回傳一個從 r
到 b
的 function。這根本就是 function composition。把 r -> a
的輸出接到 a -> b
的輸入,的確是 function composition 在做的事。如果你再仔細看看 instance 的定義,會發現真的就是一個 function composition。instance Functor ((->) r) where
fmap = (.)
這很明顯就是把
fmap
當 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
去呼叫他。到這邊為止盒子的比喻還適用嗎?如果你硬是要解釋的話還是解釋得通。當我們將
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
來做的實驗。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
,並回傳一個基於數字的 functor。那個 functor 可以是 list,可以是 Maybe
,可以是 Either String