<譯> 範疇,可大可小

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

-> 上一篇『類型與函數算法

可能你已經經過研究一些案例對範疇有所覺悟了,可是範疇是變化無窮的,它可能會在你意想不到地方蹦出來。咱們能夠從很簡單的東西上來觀察它。編程

沒有對象

最小的範疇是擁有 0 個對象的範疇。由於沒有對象,天然也就沒有態射。這個範疇挺悲哀的,由於它只擁有本身。不過,對於其它範疇而言,它多是挺重要,例如全部範疇的範疇(對的,有這麼一個範疇)。若是你以爲空集是有意義的,那麼爲什麼會以爲空的範疇無心義?segmentfault

簡單的圖

用箭頭將對象鏈接起來就能夠構造出範疇。在一個有向圖上增長一些箭頭,就能夠將它變成一個範疇。首先,要爲每一個結點增長恆等箭頭。而後爲任意兩個首尾相鄰的箭頭(也就是任意兩個可複合的箭頭)增長一個複合箭頭。每次添加一個新的箭頭,你必須得考慮它自己與其餘箭頭(除了恆等箭頭)的複合。你會畫到本身實在不想畫了,不過這樣就足夠了,這個有向圖已經變成了範疇。數據結構

從另外一個角度看這個過程,圖中每一個結點是一個對象,圖中的邊所構成就是態射。能夠認爲恆等態射就是長度爲 0 的鏈。併發

這種由給定的圖而產生的範疇,被稱爲自由範疇。它是一種自由構造的示例,即給定一個結構,用符合法則(在此,就是範疇論法則)的最小數量的東西來擴展它。接下來,會有更多的例子來講明這一點。app

如今,大相徑庭的東西出現了!有這樣一個範疇,它所包含的態射描述的是兩個對象之間的關係——小於或等於。來檢查一下它是否是一個真正的範疇。函數

Q:它有恆等態射嗎?學習

A:每一個對象都小於或等於它自身,經過!測試

Q:態射能夠複合麼?

A:若是 $a \le b$,$b \le c$,那麼 $a \le c$,經過!

Q:態射遵照結合律麼?

A:經過!

伴隨這種關係的集合被稱爲前序集,所以一個前序集其實是一個範疇。

也能夠有一個更強的關係,它知足一個附加條件,即,若是 $a\le b$,$b\le a$,那麼 $a$ 確定等於 $b$。伴隨這種關係的集合,叫偏序集。

最後,若是一個集合中的任意兩個元素之間存在偏序關係,那麼這種集合就叫作全序集。

能夠將這些有序集描繪爲範疇。前序集所構成的範疇,從任意對象 a 到任意對象 b 的態射最多隻有一個。這樣的範疇叫瘦範疇。

在一個範疇 C 中,從對象 a 到對象 b 的態射集被稱爲 hom-集,記爲 C(a,b),有時也這樣寫 $Hom_C(a,b)$。前序集內的每一個 hom-集要麼是空集,要麼是單例(Singleton)。在任一前序集構成的範疇內,C(a,a) 也是 hom-集,不過它確定是個單例,只包含着恆等態射。前序集是容許出現環的,而這種東西在偏序集內則是禁止的。

弄清楚前序、偏序與全序是很是重要的,由於排序須要它們。像快速排序、桶排序、歸併排序之類的排序算法,它們只能處理全序集。偏序集可使用拓撲排序算法來處理。

做爲集合的幺半羣

幺半羣(Monoid)是一個至關簡單可是功能強大的概念。它是基本算數幕後的概念:只要有加法或乘法運算就能夠造成幺半羣。在編程中,幺半羣無處不在。它們表現爲字符串、列表、可摺疊數據結構,併發編程中將來的一些東西,函數式響應編程中的事件,等等。

傳統的幺半羣被定義爲伴有一個二元運算的集合。這個二元運算只需知足結合律。集合中包含着一個特殊的元素,對於這個二元運算,該元素的行爲像一個返回其自身的 unit。

例如,包含 0 的天然數伴隨着加法運算就能夠造成一個幺半羣。所謂的結合律,即:

$$ (a + b) + c = a + (b + c) $$

也就是說,在數字相加的時候,括號可忽略。

那個理想是永遠保持中立的元素是 0,由於:

$$ 0 + a = a $$

以及

$$ a + 0 = a $$

第 2 個方程彷佛是多餘的,由於加法運算符合交換律,a + b = b + a,可是交換律並不是幺半羣的定義所須要。例如,字符串鏈接運算就不遵照交換律,但它能夠構成幺半羣。對於字符串鏈接運算,中立元素是空的字符串,它能夠掛接到一個字符串的任意一側,然後者依然面不改色。

在 Haskell 中,咱們能夠爲幺半羣定義一個類型類——一種包含着中立元素 mempty 並伴隨二元運算 mappend 的類型:

class Monoid m where
    mempty :: m
    mappend :: m -> m -> m

具備兩個參數的函數,其類型爲 m -> m -> m,乍一看挺古怪,可是在咱們懂得柯里化(Currying)以後,就能感覺到這種形式的美。能夠用兩種基本方式來解釋這多個箭頭的意義:(1) 一個函數有多個參數,最右邊的類型是返回值的類型;(2) 一個函數,它接受一個參數(最左邊的那個),返回一個函數。在括號的幫助下,第二種解釋能夠被直觀化爲 m -> (m -> m),不過括號是多餘的,由於箭頭是從右向左結合的。過會兒再來關注這個問題。

注意,在 Haskell 中,沒法解釋 memptymappend 的幺半羣性質,也就是說 mempty 是個什麼樣的中立者,mappend 符合怎樣的結合律。由於這是程序猿的責任,畢竟 Haskell 不能未卜先知。

Haskell 裏的類不像 C++ 的類那樣咄咄逼人。當你定義一個新的類型時,不須要聲明它所屬的類。爲一個給定的類型,聲明它是某個類的實例,這種事能夠向後延遲。例如,咱們能夠將 String 聲明爲一個幺半羣,併爲它提供 memptymappend 的實現(固然,在 Haskell 的標準庫(Standard Prelude)裏已經作了此事):

instance Monoid String where
    mempty = ""
    mappend = (++)

在此,咱們重用了列表的鏈接運算 (++),由於 String 是列表,字符列表。

簡單的說說 Haskell 的一個語法:任何中綴運算符,被括號圍住以後,就能夠轉化爲兩個參數的函數。(在學習 Haskell 時,這多是最難適應的東西。)

注意了啊,Haskell 容許函數相等。不過,在概念上,

mappend = (++)

與函數產生值時的相等

mappend s1 s2 = (++) s1 s2

是不一樣的。前者是 Hask 範疇(若是忽略底的話,是 Set)中態射的相等。像這樣的方程不只更簡潔,也常常被泛化至其餘範疇。後者被稱爲外延相等,陳述的是對於任意兩個輸入的字符串,mappend(++) 的輸出是相同的。由於參數的值有時也稱爲point(情同:f 在點 x 處的值),外延相等也被稱爲 point-wise 相等。未指定參數的函數的相等,稱爲 point-free 相等。(順便說一下,point-free 方程一般包含函數的複合,函數的複合所用的符號也是點,所以初學者可能會搞混了。)

要用現代 C++ 語言來聲明一個幺半羣,只能用概念語法(C++ 標準提案):

template<class T>
  T mempty = delete;

template<class T>
  T mappend(T, T) = delete;

template<class M>
  concept bool Monoid = requires (M m) {
    { mempty<M> } -> M;
    { mappend(m, m); } -> M;
  };

第一個定義是使用一個值的模板(也是提案)。一個多態的值是一個值的族——每一個類型的不一樣值。

關鍵詞 delete 的意思是沒有默認值,不得不根據具體狀況給它具體的值。這與 mappend 類似。

概念 Monoid 是一個謂詞,對於給定的類型 M,它測試是否存在合適的 memptymappend 的定義。

經過提供合適的特化與重載即可創建幺半羣概念的實例:

template<>
std::string mempty<std::string> = {""};

std::string mappend(std::string s1, std::string s2) {
    return s1 + s2;
}

幺半羣做爲範疇

集合形式的幺半羣,如今咱們知道了。可是你知道的,在範疇論中,咱們所嘗試的事情是放棄集合,咱們要討論的是對象與態射。所以,咱們的視角應當改變一下,從範疇的角度來看做用於集合的『移動』或『轉移』二元運算。

例如,有一個將每一個天然數都加 5 的運算,它會將 0 映射爲 5,將 1 映射爲 6,2 映射爲 7,等等。這樣就在天然數集上定義了一個函數,挺不錯的,咱們有了一個函數與一個集合。一般,對於任意數字 n,都會有一個加 n 的函數—— n 的『adder』。

這些 adder 們如何複合?加 5 的函數與加 7 的函數複合起來,是加 12。所以 adder 們的複合等同於加法規則。這也很好,咱們能夠用函數的複合來代替加法運算。

等一下,事情還沒完:還有一個 adder 是面向中立元素 0 的。加 0 不會改變任何東西,所以它是天然數集上的恆等函數。

即便不以傳統的加法規則做爲參照,照樣能給出 adder 們的複合規則。注意,adder 們的複合是符合結合律的,由於函數的複合是符合結合律的,並且咱們也有個加 0 的函數做爲恆等函數。

敏銳的讀者可能會注意到,從整型到 adder 的映射符合 mappend 類型簽名的第二種解釋,即 m -> (m -> m)。這意味着 mappend 將幺半羣的一個元素映射爲做用於這個集合的一個函數。

如今,我但願你忘掉你在處理天然數集,只是將它視爲一個單一的對象,它伴隨着一捆態射——adder 們。一個幺半羣,是一個單對象的範疇。事實上,幺半羣的名字來自希臘語 mono,它的意思是單個的。每一個幺半羣都能被表述爲帶有一個態射集的單對象範疇,這些態射皆符合複合規則。

幺半羣範疇

字符串的鏈接是一個有趣的例子,由於咱們要選擇是定義左 appender,還要定義右 appender。這兩個態射是彼此鏡象的。很容易肯定這一點,將「bar」掛到「foo」的右側,至關於將「foo」掛到「bar」的左側。

你可能會問,是否每一個範疇化的幺半羣都會定義一個惟一的伴隨二元運算的集合的幺半羣。事實上咱們老是可以從單個對象的範疇中抽出一個集合。這個集合是態射的集合——在前面的例子裏就是 adder 們。換句話說,對於只含單個對象 m 的範疇 M,咱們有一個 home-集 M(m, m)。在這個集合上,咱們很容易定義一個二元運算:兩個元素相乘至關於兩個態射的複合。若是你給我 M(m, m) 中的兩個元素 fg,它們的乘積就至關於 g∘f。複合老是存在的,由於這些態射的源對象與目標對象是同一個對象。這種乘法運算也符合範疇論法則中的結合律。恆等態射也是確定存在的。所以,咱們老是可以從幺半羣範疇中復原出幺半羣集合。不管從哪一個角度來講,它們都是同一個東西。

同態集

幺半羣的 hom-集看上去是態射,也是點集。

對於數學家的挑剔而言,有點小 bug:態射沒必要造成集合。在範疇的世界裏,有比集合更大的東西。一個範疇,其中任意兩個對象之間的態射們造成一個集合,這樣的範疇是局部小的。不過,由於承諾不要太數學,因此我會忽略這些細枝末節,在講 Haskell 的『記錄』語法時,再談論它們。

範疇論中大量的有趣現象都來自於:home-集裏的元素可被視爲遵照複合法則的態射,也可被視爲集合中的點。在此,M 中的態射的複合就會變成集合 M(m,m) 中的幺半羣式的乘法運算。

致謝

感謝 Andrew Sutton 根據他和 Bjame Stroustrup 最新的提案,重寫了個人 C++ 幺半羣概念代碼。

挑戰

  1. 從下面的東西生成自由範疇:(1) 有一個結點,沒有邊的圖;(2) 有一個結點而且有一條邊(有方向)的圖;(3) 有兩個結點而且兩者之間有一條邊的圖;(4) 有一個結點,有 26 個箭頭而且每一個箭頭標記着字母表上的一個字母的圖。
  2. 這是哪一種序?(1) 伴隨着包含關係的一組集合的集合;(2) 伴隨着子類型關係的 C++ 的類型構成的集合。
  3. Bool 是兩個值的集合,看看它能不能分別與 &&|| 構成幺半羣(集合理論中的)。
  4. 用 AND 運算表示 Bool 幺半羣:給出態射以及它們的複合法則。
  5. 將 (加 3) 與 (模 3)的複合表示爲幺半羣範疇。

-> 下一篇:『Kleisli 範疇

相關文章
相關標籤/搜索