Haskell/Functor 類
在本章中,我們將介紹重要的 Functor 型別類。
在 其他資料結構 中,我們看到了對某些分組值的所有元素適用的操作。主要例子是 map,它適用於列表。我們已經解決的另一個例子是以下 Tree 資料型別
data Tree a = Leaf a | Branch (Tree a) (Tree a) deriving (Show)
我們為 Tree 編寫的 map 函式是
treeMap :: (a -> b) -> Tree a -> Tree b
treeMap f (Leaf x) = Leaf (f x)
treeMap f (Branch left right) = Branch (treeMap f left) (treeMap f right)
正如之前討論的,我們可以設想為任何任意資料結構定義一個類似 map 的函式。
當我們在 Lists II 中首次介紹 map 時,我們經歷了將列表元素的特定函式泛化的過程,以展示 map 如何將任何合適的函式與各種列表組合在一起。現在,我們將進一步泛化。除了 map-for-lists 和 map-for-trees 以及其他不同的 map 之外,如何將所有型別的可對映型別的 map 概念進行泛化呢?
Functor 是一個 Prelude 類,用於可以對映其上的型別。它只有一個方法,稱為 fmap。該類定義如下
class Functor f where
fmap :: (a -> b) -> f a -> f b
型別變數 f 的使用一開始可能看起來有點奇怪。這裡,f 是一個引數化資料型別;在 fmap 的簽名中,f 在其一個出現中接受 a 作為型別引數,而在另一個出現中接受 b。讓我們考慮一個 Functor 的例項:透過用 Maybe 替換 f,我們得到 fmap 的以下簽名...
fmap :: (a -> b) -> Maybe a -> Maybe b
... 這符合自然定義
instance Functor Maybe where
fmap f Nothing = Nothing
fmap f (Just x) = Just (f x)
(順便說一句,這個定義在 Prelude 中;因此,我們不需要為 "其他資料結構" 一章中的那個例子真正實現 maybeMap。)
列表的 Functor 例項(也在 Prelude 中)很簡單
instance Functor [] where
fmap = map
... 並且如果我們在 fmap 簽名中用 [] 替換 f,我們將得到 map 的熟悉型別。
因此,fmap 是任何引數化資料型別的 map 的泛化。[1]
自然地,我們可以為我們自己的資料型別提供 Functor 例項。特別是,treeMap 可以立即重新分配到一個例項中
instance Functor Tree where
fmap f (Leaf x) = Leaf (f x)
fmap f (Branch left right) = Branch (fmap f left) (fmap f right)
以下是用上面的例項演示 fmap 的簡短演示(要重現它,你只需要載入 Tree 的 data 和 instance 宣告;其他的已經在 Prelude 中了)
*Main> fmap (2*) [1,2,3,4] [2,4,6,8] *Main> fmap (2*) (Just 1) Just 2 *Main> fmap (fmap (2*)) [Just 1, Just 2, Just 3, Nothing] [Just 2, Just 4, Just 6, Nothing] *Main> fmap (2*) (Branch (Branch (Leaf 1) (Leaf 2)) (Branch (Leaf 3) (Leaf 4))) Branch (Branch (Leaf 2) (Leaf 4)) (Branch (Leaf 6) (Leaf 8))
注意
除了 [] 和 Maybe 之外,還有許多其他已定義的 Functor 例項。從 Prelude 中提供的那些在 Data.Functor 模組中列出。
在提供 Functor 的新例項時,你應該確保它滿足兩個 Functor 定律。這些定律沒有什麼神秘之處;它們的作用是保證 fmap 行為正常並實際上執行對映操作(而不是其他任何無意義的操作)。[2] 第一定律是
fmap id = id
id 是恆等函式,它返回其引數而不改變。第一定律指出,在 functorial 值上對映 id 必須返回未更改的 functorial 值。
接下來,第二個定律
fmap (g . f) = fmap g . fmap f
它指出,無論我們是否對映一個組合函式,或者先對映一個函式,然後再對映另一個函式(假設兩種情況下應用順序保持相同),都不應該有任何區別。
在這一點上,我們可以問,Functor 類帶來的額外泛化層給我們帶來了什麼好處。有兩個顯著的優勢
fmap方法的可用性讓我們無需再回憶、閱讀和編寫大量不同命名的對映方法(maybeMap、treeMap、weirdMap,等等)。因此,程式碼變得既乾淨又易於理解。在發現fmap的使用時,我們立即對正在發生的事情有一個大致瞭解。[3] 由於 Functor 定律給出的保證,這個大致瞭解出奇地精確。
- 使用型別類系統,我們可以編寫基於
fmap的演算法,這些演算法可以立即與任何 Functor 一起使用 - 無論是[]、Maybe、Tree還是你需要的任何其他型別。事實上,核心庫中的許多有用類都繼承自Functor。
型別類使我們能夠為整類問題建立通用解決方案。根據你使用 Haskell 的目的,你可能不需要經常定義新類,但你一定會經常使用型別類。Haskell 的許多最強大的功能和最複雜的功能都依賴於型別類(位於標準庫中或其他地方)。從現在開始,類將是我們研究中的一個突出存在。