跳轉到內容

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

[編輯 | 編輯原始碼]

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 的簡短演示(要重現它,你只需要載入 Treedatainstance 宣告;其他的已經在 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 的新例項時,你應該確保它滿足兩個 Functor 定律。這些定律沒有什麼神秘之處;它們的作用是保證 fmap 行為正常並實際上執行對映操作(而不是其他任何無意義的操作)。[2] 第一定律是

fmap id = id

id 是恆等函式,它返回其引數而不改變。第一定律指出,在 functorial 值上對映 id 必須返回未更改的 functorial 值。

接下來,第二個定律

fmap (g . f) = fmap g . fmap f

它指出,無論我們是否對映一個組合函式,或者先對映一個函式,然後再對映另一個函式(假設兩種情況下應用順序保持相同),都不應該有任何區別。

我們得到了什麼?

[編輯 | 編輯原始碼]

在這一點上,我們可以問,Functor 類帶來的額外泛化層給我們帶來了什麼好處。有兩個顯著的優勢

  • fmap 方法的可用性讓我們無需再回憶、閱讀和編寫大量不同命名的對映方法(maybeMaptreeMapweirdMap,等等)。因此,程式碼變得既乾淨又易於理解。在發現 fmap 的使用時,我們立即對正在發生的事情有一個大致瞭解。[3] 由於 Functor 定律給出的保證,這個大致瞭解出奇地精確。
  • 使用型別類系統,我們可以編寫基於 fmap 的演算法,這些演算法可以立即與任何 Functor 一起使用 - 無論是 []MaybeTree 還是你需要的任何其他型別。事實上,核心庫中的許多有用類都繼承自 Functor

型別類使我們能夠為整類問題建立通用解決方案。根據你使用 Haskell 的目的,你可能不需要經常定義新類,但你一定會經常使用型別類。Haskell 的許多最強大的功能和最複雜的功能都依賴於型別類(位於標準庫中或其他地方)。從現在開始,類將是我們研究中的一個突出存在。

筆記

  1. 資料結構提供了最直觀的例子;但是,有一些 Functor 不能合理地被視為資料結構。一個常見的隱喻是將 Functor 視為容器;然而,就像所有隱喻一樣,它只能延伸到一定程度。
  2. 定律排除了無意義操作的一些例子:從列表中刪除或新增元素,反轉列表,將 Just 值更改為 Nothing
  3. 這類似於用基於高階函式的實現替換列表上的顯式遞迴演算法帶來的清晰度提升。
華夏公益教科書