原文見 http://bartoszmilewski.com/20...c++
上一篇文章,即《寫向程序猿的範疇論》的序言,發佈以後獲得的正面反饋讓我有些不知所措。同時,它也激勵了我,由於我感覺到了你們付諸於個人殷切指望。不過,我擔憂的是不管我如何努力,依然衆口難調。有些讀者但願這本書偏於現實,有些人則但願它能抽象一些。有些憎恨 C++ 的人但願全部的示例都是 Haskell 的,而那些憎恨 Haskell 的人又但願示例是 Java 的。我還知道內容的進展對於有些人可能太慢了,而對於有些人可能又太快了。這本書可能不會很完美,它會充滿着妥協。不過,我只指望可以與你們分享一下我頓悟時的驚喜。咱們如今從最基本的東西開始。程序員
範疇是一個至關至關至關簡單的概念。一些對象以及對象之間存在的一些箭頭就構成了一個範疇。因此,範疇很容易用圖形來表示。對象能夠畫成圓或點,箭頭就畫成箭頭。爲了好玩,有時我會把對象畫成小豬,將箭頭畫成焰火。範疇的本質是複合,若是你願意,也能夠說複合的本質是範疇。箭頭能夠複合,所以若是你有一個從 A 指向 B 的箭頭,又有一個從 B 指向 C 的箭頭,那麼就一定有一個複合箭頭——從 A 指向 C 的箭頭。編程
在範疇論中,若是有一個箭頭從 A 指向 B,又有一個箭頭從 B 指向 C,那麼就一定存在一個從 A 指向 C 的箭頭,它是前兩個箭頭的複合。這幅圖並不是一個完整的範疇,由於它沒有自態射(詳見後文)。segmentfault
如今你已經凌亂了麼?不要絕望。如今來點實在的,將箭頭想象爲函數,雖然它的學名叫態射。你有一個函數 f,它接受一個 A 類型的值,返回一個 B 類型的值。你還有一個函數 g,它接受一個 B 類型的值,返回一個 C 類型的值。你能夠將 f 的返回值傳遞給 g,這樣就完成了這兩個函數的複合,你獲得的是一個新的函數,它接受一個 A 類型的值,返回一個 C 類型的值。bash
在數學中,這樣的複合能夠用一個小圓點鏈接兩個函數來表示,即 g∘f. 注意,複合是從右向左發生的。有些人可能仍是有點不理解。你可能熟悉 Unix 中的管道,例如ide
$ lsof | grep Chrome
也可能熟悉 F# 語言中的 >>
,它們都是從左向右傳遞信息的。可是在數學與 Haskell 中,函數的複合是從右向左傳遞信息。若是你將 g∘f 讀做 g after f 可能會有助於理解。函數式編程
如今咱們來寫一些 C 代碼。咱們有一個函數 f,它接受 A 類型的參數值,返回一個 B 類型的值:函數
B f(A a);
還有一個測試
C g(B b);
那麼這兩個函數的複合,就是:網站
C g_after_f(A a) { return g(f(a)); }
此次,你能夠看到 C 中的從右向左的的複合:g(f(a))
。
我但願 C++ 標準庫中存在一個模板,它可以接受兩個函數而後返回它們的複合,可是惋惜並無這樣的模板。因此咱們只能試試 Haskell 了。下面是一個從 A 到 B 的函數的聲明:
f :: A -> B
相似的還有
g :: B -> C
它們複合爲:
g . f
一旦你見識到 Haskell 是這麼的簡單,就會以爲 C++ 在函數概念的直接表達方面顯得有些無能了。Haskell 也支持使用 Unicode 字符來寫函數的複合:
g ∘ f
也可使用 Unicode 字符來寫冒號與箭頭:
f ∷ A → B
這就是咱們的 Haskell 第一課:兩個冒號的意思是『類型爲……』。一個函數的類型是由兩個類型中間插入一個箭頭而構成的。要對兩個函數進行復合,只需在兩者之間插入一個 .
(或者 Unicode 小圓圈)。
在任何範疇中,複合必須知足兩個很是重要的性質:
1. 複合是可結合的(結合律)。若是你有三個態射,f,g 與 h,它們可以被複合(也就是它們的對象可以首尾相連),那麼你就不必在複合表達式中使用括號。在數學中,可表示爲:
h∘(g∘f) = (h∘g)∘f = h∘g∘f
在 Haskell 僞代碼(之因此說『僞』,是由於 Haskell 沒有爲函數的相等進行定義)中,可表示爲:
f :: A -> B g :: B -> C h :: C -> D h . (g . f) == (h . g) . f == h . g . f
對於函數的處理,結合律至關清晰,可是在其餘範疇中可能就不這麼清晰了。
2. 任一對象 A,都有一個箭頭,它是複合的最小單位。這個箭頭從對象出發又指向對象自身。做爲複合的最小單位,意思是當它分別與任何從 A 開始或終止於 A 的箭頭複合時,獲得的依然是與後者相同的箭頭。對象 A 的單位箭頭稱爲 idA,意思是 identity on A,即 A 與自身恆等。在數學表示中,若是 f 從 A 到 B,那麼就有
f∘idA = f
以及
idB∘f = f
在處理函數時,恆等箭頭就是做爲一個恆等函數實現的,這個函數的惟一工做就是直接返回它所接受的參數值。對於全部的類型,均可以這麼實現恆等,這意味着這個函數是多態的。在 C++ 中,咱們能夠以模板的形式來定義它:
template<class T> T id(T x) { return x; }
固然,在 C++ 中,實際狀況並不是如此簡單,由於你須要考慮要給這個函數傳遞什麼以及如何傳遞(經過值,仍是經過引用,仍是經過常量引用,仍是經過 move 語義等等)。
在 Haskell 中,恆等函數是標準庫(即 Prelude)中的一部分,其定義以下:
id :: a -> a id x = x
正如你所見,在 Haskell 中多態函數是小菜一碟,在其聲明中,你只須要用一個具體的類型來替換掉類型變量便可。這就涉及到一個小把戲:具體的類型,名字老是以一個大寫字母開頭,而類型變量的名字老是以一個小寫字母開頭。在此,a
表示全部類型。
Haskell 函數的定義由尾隨着形參的函數名構成,這裏只有一個形參 x
。函數體在 =
號以後。這種簡潔扼要的風格常常令新手愕然,但你很快就會發現它的魅力所在。函數的定義與調用是函數式編程的麪包與黃油,所以它們的語法被簡化到了骨瘦如柴的境地。參數值列表不只不須要括號,參數值之間也沒有逗號(下文在定義多個參數的函數時,就能夠看到這些)。
函數體老是由一個表達式構成,亦即函數中沒有語句。一個函數的返回結果就是這個表達式自己——在此就是 x
。
這就是咱們的 Haskell 第二課。
恆等條件可寫爲(仍是僞 Haskell 代碼):
f . id == f id . f == f
可能你會問:爲什麼須要這個什麼也不作的恆等函數?其實你應該這樣問,爲何須要數字 0?
0 是一個表示無的符號。古羅馬人有一個沒有 0 的數字系統,他們可以修建出色的道路與水渠,有些直到今天還能用。
相似 0 這樣的東西,在處理符號變量的時候特別有用。這就是羅馬人不擅長代數學的緣由,而阿拉伯人與波斯人由於熟悉 0 的概念,所以他們可以很好的掌握代數學。當恆等函數做爲高階函數的參數值或返回值時,它的價值就會得以體現。高階函數可以像處理符號那樣處理函數,它們是函數的代數。
總結一下:一個範疇由對象與箭頭(態射)構成。箭頭能夠複合,這種複合知足結合律。每一個對象都有一個恆等箭頭,它是箭頭複合的基本單位。
函數式程序員在洞察問題方面會遵循一個奇特的路線。他們首先會問一些似有禪機的問題。例如,在設計一個交互式程序時,他們會問:什麼是交互?在實現基於元胞自動機的生命遊戲時,他們可能又去沉思生命的意義。秉持這種精神,我將要問:什麼是編程?在最基本的層面,編程就是告訴計算機去作什麼,例如『從內存地址 x 處獲取內容,而後將它與寄存器 EAX 中的內容相加』。可是即便咱們使用匯編語言去編程,咱們向計算機提供的指令也是某種有意義的表達式。假設咱們正在解一個難題(若是它不難,就不必用計算機了),那麼咱們是如何求解問題的?咱們把大問題分解爲更小的問題。若是更小的問題仍是仍是很大,咱們再繼續進行分解,以此類推。最後,咱們寫出求解這些小問題的代碼,而後就出現了編程的本質:我麼將這些代碼片斷複合起來,從而產生大問題的解。若是咱們不能將代碼片斷整合起來並還原回去,那麼問題的分解就毫無心義。
層次化分解與從新複合的過程,並不是是受計算機的限制而產生,它反映的是人類思惟的侷限性。咱們的大腦一次只能處理不多的概念。生物學中被廣爲引用的一篇論文指出咱們咱們的大腦中只能保存 7± 2 個信息塊。咱們對人類短時間記憶的認識可能會有變化,可是能夠確定的是它是有限的。底線就是咱們不能處理一大堆亂糟糟的對象或像蘭州拉麪似的代碼。咱們須要結構化並不是是由於結構化的程序看上去有多麼美好,而是咱們的大腦沒法有效的處理非結構化的東西。咱們常常說一些代碼片斷是優雅的或美觀的,實際上那隻意味着它們更容易被人類有限的思惟所處理。優雅的代碼創造出尺度合理的代碼塊,它正好與咱們的『心智消化系統』可以吸取的數量相符。
那麼,對於程序的複合而言,正確的代碼塊是怎樣的?它們的表面積必需要比它們的體積增加的更爲緩慢。我喜歡這個比喻,由於幾何對象的表面積是以尺寸的平方的速度增加的,而體積是以尺寸的立方的速度增加的,所以表面積的增加速度小於體積。代碼塊的表面積是是咱們複合代碼塊時所須要的信息。代碼塊的體積是咱們爲了實現它們所須要的信息。一旦代碼塊的實現過程結束,咱們就能夠忘掉它的實現細節,只關心它與其餘代碼塊的相互影響。在面向對象編程中,類或接口的聲明就是表面。在函數式編程中,函數的聲明就是表面。我把事情簡化了一些,可是要點就是這些。
在積極阻礙咱們探視對象的內部方面,範疇論具備非凡的意義。範疇論中的一個對象,像一個星雲。對於它,你所知的只是它與其餘對象之間的關係,亦即它與其餘對象相鏈接的箭頭。這就是 Internet 搜索引擎對網站進行排名時所用的策略,它只分析輸入與輸出的連接(除非它受欺騙)。在面向對象編程中,一個理想的對象應該是隻暴露它的抽象接口(純表面,無體積),其方法則扮演箭頭的角色。若是爲了理解一個對象如何與其餘對象進行復合,當你發現不得不深刻挖掘對象的實現之時,此時你所用的編程範式的本來優點就蕩然無存了。
用你最喜歡的語言(若是你最喜歡的是 Haskell,那麼用你第二喜歡的語言)盡力實現一個恆等函數。
用你最喜歡的語言實現函數的複合,它接受兩個函數做爲參數值,返回一個它們的複合函數。
寫一個程序,測試你寫的能夠複合函數的函數是否能支持恆等函數。
互聯網是範疇嗎?連接是態射嗎?
臉書是一個以人爲對象,以朋友關係爲態射的範疇嗎?
一個有向圖,在什麼狀況下是一個範疇?
下一篇 -> 類型與函數