CWKSC's 翻譯文章

CIS 194 09 Functors 函子

Source: 09-functors

▌Functors 函子

CIS 194 第 9 週 2013 年 3 月 18 日

建議閱讀:

▌Motivation 動機

在過去的幾周中,我們看到了許多旨在將函數“映射”到某種容器的每個元素上的函數。例如:

map :: (a -> b) -> [a] -> [b]
treeMap :: (a -> b) -> Tree a -> Tree b

在作業 5 中,當您不得不以某種方式將 Maya ExprT 應用於 eval :: ExprT -> Int 以獲得 Maybe Int 時,許多人做了類似的東西

maybeEval :: (ExprT -> Int) -> Maybe ExprT -> Maybe Int
maybeMap :: (a -> b) -> Maybe a -> Maybe b

這裡有一個重複的模式,作為優秀的 Haskell 程序員,我們想知道如何概括它!那麼哪些部分在示例之間相同,哪些部分不同?

當然,不同的部分是容器被 “映射”:

thingMap :: (a -> b) -> f a -> f b

但是這些“容器”是什麼東西? 我們真的可以像 f 一樣給它們分配一個的類型變量嗎?

▌A brief digression on kinds 關於種類的簡要論述

正如每一個表達式都有一個類型,類型本身有 “類 (types)” 叫 種 (kinds)。(在您問之前:不,除了種類,沒有別的層次 —— 至少在 Haskell 中沒有)在 ghci 可以使用 :kind 詢問類型的種類。例如,讓我們問一下Int

Prelude> :k Int
Int :: *

我們看到這 Int*。實際上可以用作某些值的類型的每個類型都具有 *

Prelude> :k Bool
Bool :: *
Prelude> :k Char
Char :: *
Prelude> :k Maybe Int
Maybe Int :: *

如果 Maybe Int 的種類為 * ,那麼 Maybe 呢? 請注意,沒有 Maybe 類型的值。 有類型 Maybe IntMaybe Bool 類型的值,但沒有 Maybe 類型的值。 但 Maybe 肯定是一個有效的類。 所以那是什麼?它有什麼種類? 讓我們問一下 ghci

Prelude> :k Maybe
Maybe :: * -> *

ghci 告訴我們 Maybe* -> *Maybe 從某種意義上說,它是 類型 上的函數 —— 我們通常將其稱為類型構造函數 (type constructor)Maybe 接受一種類型的輸入類型 *,並產生另一種類型的類型 *。例如,它可以作為輸入 Int :: * 並產生新的類型 Maybe Int :: *

是否還有其他帶有 * -> * 的類型構造函數?當然,例如 Tree 或列表類型構造函數,如 []

Prelude> :k []
[] :: * -> *
Prelude :k [] Int
[] Int :: *
Prelude> :k [Int]  -- special syntax for [] Int
[Int] :: *
Prelude> :k Tree
Tree :: * -> *

那麼其他類型的構造函數呢? 作業 7 中的 JoinList 怎樣?

data JoinList m a = Empty
                  | Single m a
                  | Append m (JoinList m a) (JoinList m a)
Prelude> :k JoinList
JoinList :: * -> * -> *

這是有道理的:JoinList 期望兩種 類型用作參數,並為我們提供新的類型(當然,它是柯里化 (curried) 的,因此我們也可以將其視為接受 一種 類型並返回某種類型 * -> * )這是另一種:

Prelude> :k (->)
(->) :: * -> * -> *

這告訴我們函數類型構造函數帶有兩個類型參數。像任何運算符一樣,我們用中綴 (infix):

Prelude> :k Int -> Char
Int -> Char :: *

但是我們不必:

Prelude> :k (->) Int Char
(->) Int Char :: *

好,那這個呢?

data Funny f a = Funny a (f a)
Prelude> :k Funny
Funny :: (* -> *) -> * -> *

Funny 接受兩個參數,第一個是類型 * -> * ,第二個是類型 *,並構造一個類型。 ( GHCi 是怎麼知道 Funny 類型的呢?好吧,它也進行推斷,就像它也進行類型推斷一樣) Funny 是一個高階類型構造函數,就像 map 是一個高階函數一樣。 請注意,類型也可以像函數一樣部分地應用:

Prelude> :k Funny Maybe
Funny Maybe :: * -> *
Prelude> :k Funny Maybe Int
Funny Maybe Int :: *

▌Functor 函子

我們看到的映射模式的本質是一個具有如下類型的高階函數

thingMap :: (a -> b) -> f a -> f b

其中 f 是代表某種類型 * -> * 的類型變量。 那麼,我們可以一勞永逸地編寫此類函數嗎?

thingMap :: (a -> b) -> f a -> f b
thingMap h fa = ???

好吧,並不是的。如果我們不知道 f 是什麼,我們將無能為力。 對於每個特定的 fthingMap 必須以不同的方式工作。 解決方案是創建一個類型類,傳統上稱為 Functor

class Functor f where
  fmap :: (a -> b) -> f a -> f b

Functor 在標準 Prelude 中定義。請注意,“ functor” 的名稱來自範疇論,與 C++ 中的函子(本質上是頭等 (first-class) 函數)不同)現在,我們可以以特定於每個特定 f 的方式實現此類。 注意,Functor 是對 * -> * 類型進行抽象。 所以下面這樣寫來沒有意義:

instance Functor Int where
  fmap = ...

如果我們嘗試一下,我們會得到一個非常好的 種類不匹配錯誤

[1 of 1] Compiling Main             ( 09-functors.lhs, interpreted )

09-functors.lhs:145:19:
    Kind mis-match
    The first argument of `Functor' should have kind `* -> *',
    but `Int' has kind `*'
    In the instance declaration for `Functor Int'

如果我們了解種類,則該錯誤會告訴我們確切的錯誤所在

但是,為 Maybe 創建一個 Functor 實例確實是有意義的。 我們開始做吧。 遵循類型使其變得微不足道:

instance Functor Maybe where
  fmap _ Nothing  = Nothing
  fmap h (Just a) = Just (h a)

列表呢?

instance Functor [] where
  fmap _ []     = []
  fmap f (x:xs) = f x : fmap f xs
  -- or just
  -- fmap = map

十分簡單。那 IO 呢?為 IO 創建 Functor 實例是否有意義?

當然。fmap :: (a -> b) -> IO a -> IO b 導致 IO 操作,該 IO a 操作首先運行該操作,然後在返回結果之前應用該函數轉換結果。我們可以輕鬆實現此目標:

instance Functor IO where
  fmap f ioa = ioa >>= (\a -> return (f a))

或甚至

instance Functor IO where
  fmap f ioa = ioa >>= (return . f)

現在讓我們嘗試一些令人費解的事情:

instance Functor ((->) e) where

什麼!?好吧,讓我們按照以下類型:如果 f = (->) e 那麼我們想要

fmap :: (a -> b) -> (->) e a -> (->) e b

或者,用 (->) 書面中綴:

fmap :: (a -> b) -> (e -> a) -> (e -> b)

嗯,這種類型的簽名似乎很熟悉 ……

instance Functor ((->) e) where
  fmap = (.)

瘋!這是什麼意思?好吧,一種考慮類型值的方法 (e -> a) 是將其作為一個 “e 索引容器” , 其中每個 e 值都有一個 a 值。 要在這樣一個容器中的每個值上映射一個函數,就完全對應於函數組成:從轉換後的容器中選擇一個元素,我們首先應用 (e -> a) 函數從原始容器中選擇一個 a,然後 然後應用 (a -> b) 函數來變換我們選擇的元素

CWKSC

Author 作者

CWKSC

喜歡編程,會一點點鋼琴,會一點點畫畫,喜歡使用顏文字 About me 關於我

For any comments or discussions on my blog post, you can open an issue here

對於我博客文章的任何評論或討論,可以在這裏開一個 issue

Feel free to give your comments. OW<