<譯> 函子

原文見 http://bartoszmilewski.com/20...c++

-> 上一篇:『簡單的代數數據類型編程

聽起來是要破記錄,我要講講函子:簡單又強大的主意。範疇論中充滿了這樣簡單又強大的主意。函子是範疇之間的映射。給定的兩個範疇,C 與 D,函子 F 能夠將 C 中的對象映射爲 D 中的對象——函子是對象上的函數。若是 C 中有一個對象 a,它在 D 中的像即爲 F a(省略了括號)。可是範疇中不只僅存在對象,還有鏈接對象的態射。函子也能夠映射態射——函子是態射上的函數。不過,它不能隨意的映射態射——它須要保持態射的結構。所以,若是 C 中有一個態射 f,它鏈接對象 a 與對象 bsegmentfault

f :: a -> b

那麼 f 在 D 中的像就是 F f,它鏈接了 a 在 D 中的像與 b 在 D 中的像:編程語言

F f :: F a -> F b

以上陳述中夾雜了數學與 Haskell 記號,但願這樣有意義。對於做用於對象或態射的函子,我沒有使用括號。函數

函子

能夠看到,函子保持了範疇的結構:在一個範疇中被態射鏈接的東西在另外一個範疇中依然被相似的態射所鏈接。可是範疇的結構還包含更多的東西,即態射的複合。若是 hfg 的複合:spa

h = g . f

咱們但願它被 F 映射的像是 f 的像與 g 的像的複合:線程

F h = F g . F f

態射的複合

最後,咱們但願 C 中的恆等態射被映射爲 D 中的恆等態射:code

$$ F\; id_a = id_{F\; a} $$orm

在此,$id_a$ 是做用於對象 a 的恆等態射,而 $id_{F\;a}$ 是做用於對象 F a 的恆等態射。對象

注意,這些條件使得函子要比常規的函數更爲嚴於律己。函子必須保持範疇的結構。若是將一個範疇比喻爲對象與態射組成的一張網,函子做用於這張網的過程當中不能有一點的撕裂。它可能會將一些對象打碎,也可能會將多個態射合併爲一個,可是它永遠不能將東西從網中分割出去。這種保持網不被撕裂的約束相似於代數中的連續性條件,也就是說或函子具備『連續性』(儘管函子的連續性還存在更多的限定概念)。就像函數同樣,函子能夠作摺疊或嵌入的工做。所謂嵌入,就是將一個小的源範疇嵌入到更大的目標範疇中。一個極端的例子,源範疇是個單例範疇——只有一個對象與一個態射(恆等態射)的範疇,從單例範疇映射到任何其餘範疇的函子,所作的工做就是在後者中選擇一個對象。這徹底相似於接受單例集合的態射,這個態射會從目標集合中選擇元素。最巨大的摺疊函子被稱爲常函子 $\triangle_C$,它將源範疇中的每一個對象映射爲目標範疇中特定的對象 c,它也能夠將源範疇中的每一個態射映射爲目標範疇中的特定的恆等態射 $id_c$,它在行爲上像一個黑洞,將全部東西壓成一個奇點。在討論極限與餘極限時,咱們再來考察這個黑洞函子。

編程中的函子

如今回到地球,談談編程。咱們有了類型與函數構成的範疇。若是不知道該用函子將這個範疇映射爲別的什麼範疇,那就看看怎麼用函子將這個範疇映射爲其自身——這樣的函子被稱爲自函子。類型範疇中的一個自函子是什麼樣子的?首先,它將類型映射爲類型。咱們已經碰到過這種映射的例子了,只是你沒有意識到這一點。實際上這就是將其餘類型做爲參數的類型定義。下面來看幾個例子。

Maybe 函子

Maybe 的定義就是將類型 a 映射爲類型 Maybe a

data Maybe a = Nothing | Just a

微妙之處在於:Maybe 自己不是一個類型,它是一個類型構造子(Constructor)。必須向它提供一個類型參數,例如 IntBool,而後纔可使其變成一個類型。如不果不向 Maybe 提供任何參數,那麼它就是一個做用於類型的函數。不過,咱們能將 Maybe 變成函子麼?(從如今開始,當在編程環境中我所提到的函子,指的是自函子)一個函子不只僅只映射對象(在此,是類型),它也映射態射(在此,是函數)。對於任何從 ab 的函數:

f :: a -> b

要定義一個從 Maybe aMaybe b 的函數,須要考慮兩種狀況,它們對應於 Maybe 的兩個構造子。若這個函數的參數是 Nothing,那麼返回 Nothing 便可。若這個函數的參數是 Just,就將 f 應用於 Just 的內容。所以,fMaybe 函子映射爲:

f' :: Maybe a -> Maybe b
f' Nothing = Nothing
f' (Just x) = Just (f x)

(順便說一下,Haskell 容許在變量名中使用 ' 符號,很是方便。)Haskell 以高階函數的形式實現了一個函子的態射映射部分,這個函數叫 fmap。對於 Maybe 的狀況,這個函數的簽名以下:

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

Maybe 函子

一般說 fmap 提高了一個函數。被提高的函數做用於 Maybe 層次上的值。因爲柯里化(Curring)的緣故,fmap 的簽名有兩種解釋:做爲帶有單個參數的函數——這個參數自己是個函數 (a -> b)——返回一個函數 (Maybe a -> Maybe b);或者是帶有兩個參數的函數,返回 Maybe b

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

綜上所述,對於 Maybe 而言,fmap 的定義以下:

fmap _ Nothing = Nothing
fmap f (Just x) = Just (f x)

爲了說明類型構造子 Maybe 攜同函數 fmap 共同造成一個函子,不得不證實 fmap 可以維持恆等態射以及態射的複合的存在。所證實的東西,叫作『函子定律』。凡是知足函子定律的函子,一定不會破壞範疇的結構。

等式推導

爲了證實函子定律,我須要藉助等式推導,這也是 Haskell 中經常使用的證實技巧。它利用了 Haskell 函數基於等式定義這一優點:左側等於右側。老是能夠用其中一側替換另外一側,只是有時變量名須要改一下以免名字衝突。能夠將這種替換視爲內聯一個函數,或者將一個表達式重構爲一個函數。以恆等函數爲例:

id x = x

若是你在一些表達式中看到 id y,你能夠將其替換爲 y,這就是內聯。若是你看到 id 被應用到一個表達式,例如 id (y + 2),你能夠用這個表達式自己 (y + 2) 來替換它。這種替換能夠從兩個方向進行:你能夠用 id e 來替換 e(重構)。若是一個函數是基於模式匹配定義的,能夠單獨使用它的子定義。例如,對於上述 fmap 的定義,你能夠用 Nothing 替換 fmap f Nothing,也能夠反方向替換。下面來看如何運用等式推導。先從證實函子對恆等態射的維持開始:

fmap id = id

要考慮兩種狀況:NothingJust。第一種狀況(下面從左側到右側的變換,用的是 Haskell 僞代碼):

fmap id Nothing 
= { definition of fmap }
  Nothing 
= { definition of id }
  id Nothing

注意,上面的最後一步,我反向使用了 id 的定義。表達式 Nothing 被我替換爲 id Nothgin。在實踐中,你能夠運用這種『從兩頭點蠟燭』式的證實手段,直到它們在中間碰到相同的表達式——在此就是 Nothgin。第二種狀況也很容易:

fmap id (Just x) 
= { definition of fmap }
  Just (id x) 
= { definition of id }
  Just x
= { definition of id }
  id (Just x)

如今來證實 fmap 可以維持態射的複合:

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

首先來看 Nothing 對應的狀況:

fmap (g . f) Nothing 
= { definition of fmap }
  Nothing 
= { definition of fmap }
  fmap g Nothing
= { definition of fmap }
  fmap g (fmap f Nothing)

再來看 Just 對應的狀況:

fmap (g . f) (Just x)
= { definition of fmap }
  Just ((g . f) x)
= { definition of composition }
  Just (g (f x))
= { definition of fmap }
  fmap g (Just (f x))
= { definition of fmap }
  fmap g (fmap f (Just x))
= { definition of composition }
  (fmap g . fmap f) (Just x)

須要強調一下,帶有反作用的 C++ 『函數』是不能用等式推導的,請看:

int square(int x) {
    return x * x;
}

int counter() {
    static int c = 0;
    return c++;
}

double y = square(counter());

使用等式推導,你能夠內聯 square,獲得:

double y = counter() * counter();

結果絕對不是一個有效的變換,由於它沒法產生相同的結果。若是使用宏來定義 square,C++ 編譯器會嘗試使用等式推導,可是結果可能挺悲催。

Optional

在 Haskell 中很容易表示函子,可是在其餘任何支持泛型編程與高階函數的語言中也可以定義函子。C++ 中的 Maybe 是模板類型 optional,下面是它的粗略實現(真實的實現至關複雜,須要處理參數傳遞的多種方式,要用到 Copy 構造函數以及資源管理等 C++ 特點的東西):

template<class T>
class optional {
    bool _isValid; // the tag
    T    _v;
public:
    optional()    : _isValid(false) {}         // Nothing
    optional(T x) : _isValid(true) , _v(x) {}  // Just
    bool isValid() const { return _isValid; }
    T val() const { return _v; }
};

這個模板提供了函子定義的一部分:類型的映射。它將任意類型 T 映射爲一個新的類型 optional<T>。函子定義的另外一部分,即函數的映射,實現以下:

template<class A, class B>
std::function<optional<B>(optional<A>)> 
fmap(std::function<B(A)> f) 
{
    return [f](optional<A> opt) {
        if (!opt.isValid())
            return optional<B>{};
        else
            return optional<B>{ f(opt.val()) };
    };
}

這是個高階函數,它接受一個函數做爲參數,而後返回一個函數。它的非柯里化版本以下:

template<class A, class B>
optional<B> fmap(std::function<B(A)> f, optional<A> opt) {
    if (!opt.isValid())
        return optional<B>{};
    else
        return optional<B>{ f(opt.val()) };
}

也能夠將 fmap 定義爲 optional 的模板方法。這種選擇上的困窘,使得函子模式在 C++ 中的抽象變成一個問題。是讓函子做爲接口來繼承(不幸的是,模板類型是不能擁有虛函數的),仍是讓函子做爲柯里化或非柯里化的自由的模板函數?是讓 C++ 編譯器去正確的推導類型,仍是顯式的指定類型?思考一下,若函子接受一個從 intbool 的函數 f,那麼編譯器當如何肯定 g 的類型:

auto g = fmap(f);

特別是,在將來,若是有多個重載了 fmap 的函子呢?(很快咱們就會看到更多的函子)

類型類

Haskell 如何對函子進行抽象?它使用類型類。一個類型類定義了支持一個公共接口的類型族。例如,支持相等謂詞的類型類以下:

class Eq a where
    (==) :: a -> a -> Bool

這個定義陳述的是,若是類型 a 支持 (==) 運算符,那麼它就是 Eq 類。(==) 運算符接受兩個類型爲 a 的值,返回 Bool 值。若是你想告訴 Haskell 有一個特定的類型是 Eq 類,那麼你不得不將其聲明爲這個類的一個實例,並提供 (==) 的實現。例如,一個二維 Point(兩個 Float 的積類型):

data Point = Pt Float Float

須要爲它定義相等謂詞:

instance Eq Point where
    (Pt x y) == (Pt x' y') = x == x' && y == y'

這裏我將 (==) 做爲中綴運算符使用,它處於 (Pt x y)(Pt x' y') 之間,而函數體是單個 = 號後面的部分。一旦將 Point 聲明爲 Eq 的一個實例,你就能夠直接比較點與點是否相等了。注意,與 C++ 或 Java 不一樣,在定義 Point 的時候沒必要指定它是 Eq 類(或接口)的實例——可在真正須要的時候再指定。類型類是 Haskell 僅有的函數(運算符)重載機制。在爲不一樣的函子重載 fmap 時須要藉助類型類,儘管有一個難點:函子不能做爲類型來定義,只能做爲類型的映射來定義,即類型構造子。咱們須要一個由類型構造子構成的族,而不是像 Eq 這樣的類型族。所幸,Haskell 的類型類能夠將類型構造子像類型那樣來處理。所以,Functor 類的定義以下:

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

若是存在符合上述簽名的 fmap 函數,這個類規定了 f 是一個 Functor。小寫的 f 是一個類型變量,相似於類型變量 ab,然而編譯器可以推斷出它是一個類型構造子,而不是一個類型,依據是它的用途:做用於其餘類型,即 f af b。所以,要聲明一個 Functor 的實例時,你不得不給它一個類型構造子,對於 Maybe 而言就是:

instance Functor Maybe where
    fmap _ Nothing = Nothing
    fmap f (Just x) = Just (f x)

順便說一下,Functor 類,以及它的一些實例,這些實例是爲大多數簡單的數據類型而定義的,包括 Maybe,它們都是 Haskell 標準庫的一部分。

C++ 中的函子

在 C++ 中能夠弄成相似的函子類麼?一個類型構造子對應於一個模板類,例如 optional,所以做一樣的類比,咱們能夠用一個*模板的模板的參數 F 來參數化 fmap,即:

template<template<class> F, class A, class B>
F<B> fmap(std::function<B(A)>, F<A>);

對於不一樣的函子,咱們想對這個模板進行特化。不幸的是,在 C++ 中是禁止模板函數的部分特化的。因此,你不能這樣寫:

template<class A, class B>
optional<B> fmap<optional>(std::function<B(A)> f, optional<A> opt)

咱們只能再回到函數重載的老路上,回到那個非柯里化版本的 fmap 的原始定義:

template<class A, class B>
optional<B> fmap(std::function<B(A)> f, optional<A> opt) 
{
    if (!opt.isValid())
        return optional<B>{};
    else
        return optional<B>{ f(opt.val()) };
}

這個定義可以工做,但只是由於編譯器根據 fmap 第二個參數選擇了一個重載版本的 fmap,它徹底忽略了那個更爲泛型的 fmap 定義。

List 函子

爲了對編程中的函子所扮演的角色得到一些直覺,咱們須要看更多的例子。任意一個被其餘類型參數化了的類型,都是一個候選的函子。通用的容器被它們所存儲的類型參數化了,來看一個最簡單的容器——列表:

data List a = Nil | Cons a (List a)

List 是類型構造子,它將任意類型 a 映射爲類型 List a。爲了代表 List 是一個函子,咱們不得不定義一個提高函數:接受一個 a -> b 的函數,產生一個 List a -> List b 的函數:

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

做用於 List a 的函數必需要考慮兩種狀況,由於存在兩個列表構造子。Nil 對應狀況很簡單——只需返回 Nil——你不可能對一個空的列表作別的什麼事。Cons 對應的狀況有點麻煩,由於它包含着遞歸。咱們先退一步,考慮一下正在幹什麼。咱們有一個 a 的列表(包含類型 a 的值的列表),還有一個從 ab 的函數,咱們想獲得的是一個 b 的列表(包含類型 b 的值的列表)。顯然,咱們要用 fa 的列表中的每個元素映射到 b 的列表中。假設各定的是由 Cons 定義了首尾的一個(非空的)列表,如何實現這樣的函數?咱們能夠將 f 做用於列表的首部,而後將提高後的(fmap 過的)f 做用於列表的尾部。這依然是一個遞歸的定義,由於咱們是以提高的 f 來定義提高的 f

fmap f (Cons x t) = Cons (f x) (fmap f t)

注意,等式的右側,fmap f 做用於一個列表,而這個列表的長度小於等式左側傳入的那個列表——前者是後者的尾部。這個遞歸過程逐漸將列表縮短,最終會抵達一個空的列表,即 Nil。因爲咱們已經定義了做用於 Nilfmap f 的返回結果是 Nil,所以遞歸過程就終止了。爲了獲得最終的結果,咱們將新的首部 (f x) 與新的尾部 (fmap f t) 使用 Cons 構造子組裝起來。將上述所定義的東西放到一塊兒,就獲得了列表函子的實例聲明:

instance Functor List where
    fmap _ Nil = Nil
    fmap f (Cons x t) = Cons (f x) (fmap f t)

若是你足夠熟悉 C++,會想到 std::vector,它是 C++ 中應用最爲普遍的容器。面向 std::vectorfmap 能夠經過薄層封裝 std::transform 來實現:

template<class A, class B>
std::vector<B> fmap(std::function<B(A)> f, std::vector<A> v)
{
    std::vector<B> w;
    std::transform( std::begin(v)
                  , std::end(v)
                  , std::back_inserter(w)
                  , f);
    return w;
}

咱們能夠用這個函數來計算一系列數值的平方:

std::vector<int> v{ 1, 2, 3, 4 };
auto w = fmap([](int i) { return i*i; }, v);
std::copy( std::begin(w)
         , std::end(w)
         , std::ostream_iterator(std::cout, ", "));

大部分 C++ 容器都是函子,它們依賴於能夠傳給 std::transform 的迭代器。不行的是,函子的單純性在迭代器與臨時建築(看看上面的 fmap 的實現)的混亂背景下喪失了。我很高興的宣佈,新的 C++ range 庫更加函數化了。

Reader 函子

你已經對函子得到一些直覺了——例如,函子是某種容器——下面來個有點燒腦的例子。考慮一個從類型 a 到一個返回 a 的函數類型的映射。咱們不去深刻的探討函數類型——這須要全面的範疇知識——不過,咱們是程序猿,對函數類型老是有一些認識的。在 Haskell 中,函數類型是使用箭頭類型構造子 (->) 構造出來的,這個類型構造子接受兩種類型:參數類型與返回類型。你已經見過這個類型構造子的中綴形式 a -> b,可是也能夠寫成前綴形式,像是被參數化了:

(->) a b

就像常規的函數同樣,接受多個參數的類型函數能夠偏應用。所以,當咱們向箭頭只提供一個參數時,它依然期待另外一個參數的出現。所以

(->) a

也是個類型構造子。它須要一個類型 b 來產生完整的類型 a -> b。它所表示的是,它定義了一族由 a 參數化的類型構造子。咱們看一下是否是還有個函子族。處理兩個類型參數可能有點混亂,先作一些重命名的工做。在咱們以前的函子定義中,咱們能夠將參數類型稱爲 r,將返回類型稱爲 a。所以咱們的類型構造子能夠接受任意類型 a,並將其映射爲類型 r -> a。爲了代表它是個函子,咱們就須要一個函數,它能夠將函數 a ->b 提高爲一個從 r -> ar -> b 的函數,而 r -> ar -> b 就是 (->) r 這個類型構造子分別做用於 ab 所產生的函數類型。以上討論最終可歸結爲 fmap 的函數簽名:

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

咱們不得不解決一個難題:對於給定的函數 f :: a -> bg :: r -> a,構造一個函數 r -> b。這是複合兩個函數的惟一途徑,也偏偏就是咱們須要的。所以,咱們須要將 fmap 的實現爲:

instance Functor ((->) r) where
    fmap f g = f . g

這就是咱們想要的 fmap!若是你喜歡緊湊一些的表示,能夠先將上面等式的右側改成前綴表示:

fmap f g = (.) f g

而後忽略等公兩側的參數:

fmap = (.)

類型構造子 (->) r 與上面這個 fmap 組合起來所造成的函子被稱爲 Reader 函子。

做爲容器的函子

咱們已經見識了編程語言中定義了通用容器的函子,至少它們定義了一些包含了某些類型的值的對象,也就是說這些對象被自身所包含的值的類型參數化了。Reader 函子看上去是個異類,由於咱們沒有想過將函數視爲數據。不過,咱們已經見過純函數是能夠被保存下來的,函數的執行結果被扔到可檢索的表中,而表是數據。反之,因爲 Haskell 具備惰性計算能力,一個傳統的容器,譬如一個列表,實際上能夠被實現爲一個函數。例如,一個包含天然數的無限長的列表,可被定義爲:

nats :: [Integer]
nats = [1..]

第一行,方括號是 Haskell 內建的列表類型構造子。第二行,方括號用於構造列表數據。顯然,一個無限長的列表是不能存儲在內存中的。編譯器將其實現爲一個能夠按需產生一組 Integer 值的函數。Haskell 有效的模糊了數據與代碼的區別。能夠將列表視爲函數,也能夠將函數視爲從存儲着參數與結果的表中查詢數據。若是函數的定義域有界而且不太大,將函數變成表查詢是徹底可行的。strlen 不能變成表查詢,由於有無限多的不一樣的字符串。做爲程序猿,咱們不喜歡無限,可是在範疇論中無限是屢見不鮮。不管是全部字符串的集合仍是宇宙全部可能狀態的集合——過去,如今與將來——老是能夠在範疇論中處理!所以,我喜歡將函子對象(由自函子產生的類型的實例)視爲包含着一個值或多個值的容器,即便這些值實際上並未出場。C++ 中的 std::future 函子在某些時間點上能夠存儲一個值,可是它不能擔保這個值老是存在;若是你想訪問這個值,可能會受阻,直到其它線程的計算過程。另外一個例子是 Haskell 的 IO 對象,它包含着用戶的輸入,或者是咱們的宇宙要顯示於屏幕上的『Hello World!』的將來版本。根據這種解釋,函子對象就是能夠包含一個值或多個值的容器,這些值的類型參數化了函子對象。或者,函子對象可能包含產生這些值的方法。咱們根本不關心可否訪問這些值——這些事發生在函子做用範圍以外。若是函子對象包含的值可以被訪問,咱們就能夠看到相應的操做結果;若是它們不能被訪問,咱們所關心的只是操做的正確複合,以及伴隨不改變任何事物的恆等函數的操做。爲了向你代表咱們是如何的不關心函子對象內部的值,這裏有一個類型構造子,它徹底的忽略參數 a

data Const c a = Const c

Const 類型構造子接受兩種類型, ca。就像咱們處理箭頭構造子那樣,咱們對其進行偏應用從而製造了一個函子。數據構造子(也叫 Const)僅接受 c 類型的值,它不依賴 a。與這種類型構造子相配的 fmap 類型爲:

fmap :: (a -> b) -> Const c a -> Const c b

由於這個函子是忽略類型參數的,因此 fmap 的實現也能夠自由忽略那個函數參數——由於這個函數無事可作:

instance Functor (Const c) where
    fmap _ (Const v) = Const v

在 C++ 中可能更清晰一些(我從未想過我竟然會這樣說!),由於 C++ 中在類型參數與值之間有着明顯的區別,前者出現於編譯期間,後者出現於運行時:

template<class C, class A>
struct Const {
    Const(C v) : _v(v) {}
    C _v;
};

C++ 版本的 fmap 也能夠忽略那個函數參數,本質上就是不改變 Const 參數所包含的值的類型轉換:

template<class C, class A, class B>
Const<C, B> fmap(std::function<B(A)> f, Const<C, A> c) {
    return Const<C, B>{c._v};
}

儘管它有些怪異,可是 Const 函子在許多結構中扮演着重要的角色。在範疇論中,它是 $\triangle_C$ 函子的特例,後者咱們在前面提到過的,就是那個黑洞函子,而 Const 是個黑洞自函子。未來,咱們還會碰到它。

函子的複合

讓本身相信範疇之間的函子能夠複合並不太難,函子的複合相似於集合之間的函數複合。兩個函子的複合,就是兩個函子分別對各自的對象進行映射的複合,對於態射也是這樣。恆等態射穿過兩個函子以後,它仍是恆等態射。複合的態射穿過兩個函子以後仍是複合的態射。函子的複合只涉及這些東西。特別是,自函子很容易複合。還記得 maybeTail 麼?下面我用 Haskell 內建的列表來從新實現它(用 [] 替換 Nil,用 : 替換 Cons):

maybeTail :: [a] -> Maybe [a]
maybeTail [] = Nothing
maybeTail (x:xs) = Just xs

maybeTail 返回的結果是兩個做用於 a 的函子 Maybe[] 複合後的類型。這些函子,每個都配備了一個 fmap,可是若是咱們想將一個函數 f 做用於複合的函子 Maybe [] 所包含的內容,該怎麼作?咱們不得不突破兩層函子的封裝:使用 fmap 突破 Maybe,再使用 fmap 突破列表。例如,要對一個 Maybe [Int] 中所包含的元素求平方,能夠這樣作:

square x = x * x

mis :: Maybe [Int]
mis = Just [1, 2, 3]

mis2 = fmap (fmap square) mis

通過類型分析,對於外部的 fmap,編譯器會使用 Maybe 版本的;對於內部的 fmap,編譯器會使用列表版本的。因而,上述代碼也能夠寫爲:

mis2 = (fmap . fmap) square mis

不過,要記住,fmap 能夠看做是隻接受一個參數的函數:

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

在咱們的示例中,(fmap . fmap) 中的第 2 個 fmap 所接受的參數是:

square :: Int -> Int

而後返回一個這種類型的函數:

[Int] -> [Int]

第一個 fmap 接受這個函數,而後再返回一個函數:

Maybe [Int] -> Maybe [Int]

最終,這個函數做用於 mis。所以兩個函子的複合結果,依然是函子,而且這個函子的 fmap 是那兩個函子對應的 fmap 的複合。如今回到範疇論:顯然函子的複合是遵照結合律的,由於對象的映射遵照結合律,態射的映射也遵照結合律。在每一個範疇中也有一個恆等函子:它將每一個對象都映射爲其自身,將每一個態射映射爲其自身。所以在某個範疇中,函子具備與態射相同的性質。什麼範疇會是這個樣子?必須得有一個範疇,它包含的對象是範疇,它包含的態射是函子。也就是說,它是範疇的範疇。可是,全部範疇的範疇不得不包含它自身,這樣咱們就陷入了自相矛盾的境地,就像不可能存在集合的集合那樣。然而,有一個叫作 Cat 的範疇,它包含了全部的範疇。這個範疇是一個大的範疇,所以它就不多是它自身的成員。所謂的小范疇,就是它包含的對象能夠造成一個集合,而不是某種比集合還大的東西。請注意,在範疇論中,即便一個無限的不可數的集合也被認爲是『小』的。我想我已經提到過這樣的集合了,由於咱們已經看過一樣的結構在許多抽象層次上的重複出現。之後咱們也會看到函子也能造成範疇。

挑戰

1. 下面的定義能夠將類型構造子 Maybe 變成一個函子嗎?(提示:檢查它是否符合函子定律)

fmap _ _ = Nothing

2. 證實 Reader 函子符合函子定律。(提示:這至關簡單。)

3. 用你第二喜歡的語言實現 Reader 函子(第一個固然是 Haskell)。

4. 證實列表函子符合函子定律。假設列表尾部函子符合函子定律(換句話說,使用概括法。)

致謝

Gershom Bazerman 至關友善的審閱了這一系列文章。很是感謝他的耐心和看法。

-> 下一篇:『函子性

相關文章
相關標籤/搜索