可以映射的 Functor
February 11, 2022在〈流程抽象〉探討過函數式設計中一些簡單的流程抽象,有些型態具有類似的流程抽象,這類抽象會被進一步抽取出來,定義為型態類別(Typeclass),而型態成為型態類別的實例。
流程抽象經由抽取再抽取,會構成一些看似高深莫測的型態類別,不少初學者認為這些行為難以理解,某些程度上,這是因為沒有先去觀察過具體實作,直接從抽象去理解,才會造成這種誤會。
例如,來看看 Functor
:
class Functor f where
fmap :: (a -> b) -> f a -> f b
喔!如果你第一次接觸 Functor
,一定看不懂這啥鬼!還是先來看看 Maybe
!你應該還記得它吧!忘了或沒看過的話,先回頭看看〈Maybe 有無、Either 對錯〉。
從 Maybe a 到 Maybe b
函式呼叫結果若會有或沒有值時,可以傳回 Maybe
,如果你拿到了一個 Maybe
型態的值,基本上要判斷是 Just something
或 Nothing
後採取進一步動作,例如:
customerName :: Maybe Int -> Maybe String
customerName (Just orderId) = Just (customer orderId)
customerName Nothing = Nothing
上面的 customName
函式可以給個 Maybe Int
,其中 Int
是訂單 id,而 customer
會查找對應訂單的客戶名稱;或許你也會想要取得客戶的住址資訊:
customerAddress :: Maybe Int -> Maybe String
customerAddress (Just orderId) = Just (address orderId)
customerAddress Nothing = Nothing
顯然地,兩者具有類似的流程,只是當中使用了 customer
或是 address
,既然如此,不如定義一個通用的 fmap
函式,讓使用者可以指定 a -> b
型態的函式:
fmap' :: (a -> b) -> Maybe a -> Maybe b
fmap' mapper (Just x) = Just (mapper x)
fmap' mapper Nothing = Nothing
那麼,customerName
與 customerAddress
就可以分別改寫如下:
customerName :: Maybe Int -> Maybe String
customerName maybeOrderId = fmap' customer maybeOrderId
customerAddress :: Maybe String -> Maybe String
customerAddress maybeOrderId = fmap' city maybeOrderId
進一步地,可以寫為 Point free 風格:
customerName :: Maybe Int -> Maybe String
customerName = fmap' customer
customerAddress :: Maybe Int -> Maybe String
customerAddress = fmap' city
從 [a] 到 [b]
方才實現的 fmap'
,可以指定 a -> b
型態的函式,這句話聽起來很熟悉?map
函式不也是如此?還記得 map
的型態嗎?
ghci> :t map
map :: (a -> b) -> [a] -> [b]
ghci>
方才實現的 fmap'
,型態是 (a -> b) -> Maybe a -> Maybe b
,而 map
的型態是 (a -> b) -> [a] -> [b]
,其實就相當於 (a -> b) -> List a -> List b
,兩個函式的型態只差別在一個是 Maybe
、一個是 List
,而兩個函式的實作一個是針對 Maybe
,一個是針對 List
。
在〈型態類別定義、實作與衍生〉談過,可以將共同行為定義為型態類別,然後由各型態實現行為,既然如此,就來用型態類別定義一個 Functor'
,其中 Maybe
、List
的部份未定,那就放個型態參數 f
:
class Functor' f where
fmap :: (a -> b) -> f a -> f b
嗯?這不就像是這篇文件一開始的 Functor
定義嗎?而且,因為已經有 map
函式,要 list 實現 Functor'
的行為,只要令 fmap
為 map
就可以了:
instance Functor' [] where
fmap = map
Haskell 本身就定義了 Functor
,list 就是 Functor
的實例,因此可以使用 fmap
:
ghci> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f b
ghci> fmap show [1, 2, 3]
["1","2","3"]
ghci>
也就是說,你可以指定 a -> b
函式,以便將 f a
對應至 f b
,就上例而言,f
就是 List
罷了。
Functor 的行為
重新來看 Haskell 的 Functor
定義,這邊的 f
是型態參數,f 不是 function 的縮寫,而是代表 Functor
的縮寫:
class Functor f where
fmap :: (a -> b) -> f a -> f b
具有行為 Functor
的型態,要實現 fmap
行為,例如,Maybe
若要自行實現 Functor
行為,可以如下:
instance Functor Maybe where
fmap :: (a -> b) -> Maybe a -> Maybe b
fmap mapper (Just x) = Just (mapper x)
fmap mapper Nothing = Nothing
實際上 Maybe
早就是 Haskell 內建 Functor
的實例,因此也可以使用 fmap
:
ghci> fmap show (Just 10)
Just "10"
ghci>
也就是說,Functor
的實例必須具有的行為是,能接受一個 a -> b
的函式,以便將 f a
對應至 f b
,就以上的例子而言,f
是 Maybe
。
如果將 List
、Maybe
看成是值的容器,那麼具有 Functor
行為的型態,其 fmap
實現,可以將容器與值進行映射,成為另一同型態容器與值的組合,像是 Maybe a -> Maybe b
,或者是 [a] -> [b]
。
從 IO a 到 IO b
在 Haskell 中,你可能寫過以下類似的程式:
main = do
input <- getLine
let result = (show . (*2) . read) input
putStrLn result
getLine
的傳回型態是 IO String
,因此要將結果綁定到一個名稱,以便傳給其他函式,在這邊其他函式是指 read
、(*2)
、show
這些函式,這些函式最後傳回了一個 Int
。
IO
也是 Functor
的實例喔!在實作行為時的定義是這樣的:
instance Functor IO where
fmap f action = do
result <- action
return (f result)
也就是說,IO
可以適用 fmap
,你可以指定 a -> b
函式,以便從 IO a
對應至 IO b
,因此你可以這麼寫:
main = do
result <- fmap (show . (*2) . read) getLine
putStrLn result
show . (*2) . read
組成了 String -> Int
的函式,因此 fmap (show . (*2) . read) getLine
的結果,就是將 IO String
對應至 IO Int
。
因此,如果想將 IO something
的結果綁定到一個名稱,以便傳給其他函式,可以考慮使用 fmap
,讓程式看來簡潔一些。
函式提昇
方才談到,fmap
函式的型態是什麼呢?
ghci> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f b
ghci>
如果你部份套用 fmap
的話呢?
ghci> let fmapPlus10 = fmap (+ 10)
ghci> :t fmapPlus10
fmapPlus10 :: (Functor f, Num b) => f b -> f b
ghci>
喔!這可有趣了,你得到了一個 f b -> f b
的函式,f
必須是 Functor
,像是 Maybe
或 List
:
ghci> fmapPlus10 (Just 10)
Just 20
ghci> fmapPlus10 [10, 20, 30]
[20,30,40]
ghci>
來看看另一個例子:
ghci> let fmapShow = fmap show
ghci> :t fmapShow
fmapShow :: (Functor f, Show a) => f a -> f String
ghci> fmapShow (Just 100)
Just "100"
ghci> fmapShow [100, 200, 300]
["100","200","300"]
ghci>
對 fmap
部份套用 a -> b
函式,就像是給 fmap
一個 a -> b
函式,傳回一個 f a -> f b
的函式,就像是將函式 a -> b
提昇(lift)為 f a -> f b
。
Functor 定律
Functor
在實作 fmap
時有必須遵合的定律,別被定律這兩個字嚇到,其實就是實作規範,這在 Data.Functor 的文件中也有定義:
fmap id == id
fmap (f . g) == fmap f . fmap g
以上兩條分別對應的陳述就是:
fmap
該做的事,就只是按照指定的函式做對應,不做多餘的事,因此,對fmap
指定id
恒等函式,傳回的函式在進行Functor
的對應,其結果應該與對Functor
執行id
相同。fmap
指定的函式,若為數個函式合成,那對Functor
的執行結果,應與數個函式分別進行fmap
相同。