Bartosz Milewski 的部落格上寫了很多關於 C++ 模板 與 Haskell 關係的相關文章,讀來真是受益良多,這位大哥很多年前就開始探討 c++ 模板程式設計與 Haskell 之間的微妙聯絡,許多觀點讓人眼前一亮以至歎為觀止,比如說從範疇論的角度來理解和解釋什麼是單子(monad)(我接下來準備寫篇部落格總結一下)。Bartosz 講 Haskell 喜歡從數學的角度來闡述,視角非同一般,當然他不是第一位這樣做的,事實上 Haskell 與數學本來就有著許多不得不說又說不清道不明的曖昧關係(住口!)。
範疇論基本概念
如果你是第一次聽說範疇論,看到這高大上的名字估計心裡就會一咯噔,到底數學威力巨大,光是高等數學就讓很多人噩夢連連。和搞程式設計的一樣,數學家喜歡將問題不斷加以抽象從而將本質問題抽取出來加以論證解決,範疇論就是這樣一門以抽象的方法來處理數學概念的學科,主要用於研究一些數學結構之間的關係及聯絡。
在範疇論裡,一個範疇(category)指的是這樣一個東西,它由三部分組成:
- 一系列的物件(object).
- 一系列的態射(morphism).
- 一個組合(composition)操作符,用點(.)表示,用於將態射進行組合。
一個態射指的是一種對映關係,簡單理解,態射的作用就是把一個物件 A 裡的值 va 對映為 另一個物件 B 裡的值 vb,這和代數裡的對映概念是很相近的,因此也有單射,滿射等區分。態射的存在反映了物件內部的結構,這是範疇論用來研究物件的主要手法:物件內部的結構特性是通過與別的物件的關係反映出來的,動靜是相對的,範疇論通過研究關係來達到探知物件的內部結構的目的。
組合操作符的作用是將兩個態射進行組合,例如,假設存在態射 f: A -> B, g: B -> C, 則 g.f : A -> c.
看!好像沒有想象中的複雜!一個結構要想成為一個範疇, 除了必須包含上述三樣東西,它還要滿足以下三個限制:
- 態射要滿足結合律,即 f.(g.h) = (f.g).h。
- 態射在這個結構必須是封閉的,也就是,如果存在態射 f, g,則必然存在 h = f.g。
- 對結構中的每一個物件 A, 必須存在一個單位態射 Ia: A -> A, 對單位態射,顯然,對任意其它態射 f, f.I = f。
Haskell 中的範疇
在 Haskell 中存在著這樣一個唯一的範疇,名字稱為 Hask, 這個 Hask 滿足前面關於範疇的全部約定,因此是範疇論裡一個純正的“範疇":
- 物件就是 Haskell 裡的所有型別,記得型別是一個集合。
- 態射就是程式語言裡的一般函式(function),如: func :: Int -> Bool,將物件 int 對映為 物件 bool。
- 態射的組合就是函式的組合,在 Haskell 裡,函式也是通過點號(.)進行組合的。
函子
前面對範疇的介紹反映了範疇內部各個物件之間的聯絡與相互作用,在範疇論裡另外研究的重點是範疇與範疇之間的關係,就正如物件與物件之間有態射一樣,範疇與範疇之間也存在某些對映,從而可以將一個範疇對映為另一個範疇,這種對映在範疇論中叫作函子(functor),具體來說,對於給定的兩個範疇 A 和 B, 函子的作用有兩個:
- 將範疇 A 中的物件對映到範疇 B 中的物件。
- 將範疇 A 中的態射對映到範疇 B 中的態射。
- 對範疇 A 上的單位態射Ia, F 必須將其對映為範疇 B 上的單位態射 Ib, F(Ia) = Ib.
- 函子對態射的組合必須滿足分配徤,即,假設 f, g 是範疇 A 上的態射,則 F(f.h) = F(f).F(g)。
和態射一樣函子也可以是自對映的,即函子允許將範疇對映到其自身,這樣做有什麼好處呢?不同範疇之間的對映反映了範疇間的相似性,範疇到範疇自身的對映則顯然是反映了範疇內部的自相似性 --- 到底認識自己也不是一件容易的事啊。。。自相似性是大自然里美妙的存在,想想六角形的雪花,想想分形... 在範疇論裡,這種將範疇對映到自身的函子被稱為自函子(endofunctor).
Haskell 中的函子
知道為什麼要講自函子了嗎,Haskell 中只有一個範疇! 那麼這個唯一的範疇 Hask 中,存不存在自函子呢?有的!終於講到重點了,為什麼 Haskell 有這麼些奇怪的概念? Haskell 的老鳥會告訴你,這些奇怪的東西都是寶貝,它們都是有本而來的。
那麼 Haskell 中的自函子是怎麼體現出來的呢? 根據前面的定義,一個函子其實就是一個對映,它把物件對映為物件,把態射對映為態射,我們知道在 Haskell 中物件就是一個型別,如整型,布林型等,將一個型別對映為另一個型別,沒錯,就是 type constructor 在乾的事情,c++ 的程式設計師可以用模板類來想象一下,如,vector<int> 就是將 int 對映為 vector<int>, 這是兩種不同的型別了,例項化模板的過程實際就是把一個型別變成另一個型別的過程。
注意不要把物件的對映與物件內部的態射混淆了,態射是將物件內部的值進行對映,而物件的對映(函子)是把物件這個整體對映為另一個物件,函子根本不關心一個物件內部會有什麼值。
顯然我們可以看到,在 Haskell 中,型別到型別的對映事實上並不是普遍存在的,自函子反映的是範疇內部的結構關係,這些關係並不是因為函子的存在而存在,函子只是揭示了這些內在的關係。具體在 Haskell 中,型別間的關係並不是普遍存在的,比如說, Int -> Bool 就沒有對應的對映關係,而存在對映關係的型別,它們都有一些共同的特點,對映雙方可以看成是由簡單的型別轉變為複雜的型別。
type constructor 就是自函子的一部分!
好了,現在型別到型別的對映在 Haskell 中找到了,那態射到態射之間的對映呢?必竟這也是函子的必要組成部分。
在 Haskell 中,態射就是一般的函式,把一個函式對映為另一個函式,聽起來不就是高階函式在乾的事情嘛。具體來說,對映函式這件事發生在 Functor 這個 typeclass 裡,連名字都一模一樣,目的昭然若揭。Haskell 中 Functor 是一個 typeclass,它的定義如下:
class Functor f where fmap:: (a -> b) -> f a -> fbfmap 幹嘛的?顯然就是用於把態射 (a -> b) 對映為態射 (f a -> f b)的,它把範疇裡的態射對映到另一個態射,且遵守了函子在對映態射時所需要遵守的兩個原則。
講到這裡,我們一步一步不知不覺就已經向著 monad 靠近了,好激動,先打住了,回頭再整理整理。
相關閱讀
評論(2)