說到 Haskell,這真是一門逼格極高的編程語言,一般初學者如果沒有相關函數式編程的經驗,入門直接接觸那些稀奇古怪的概念,簡直要跪下。現在回想起來,隱隱覺得初學者所擁有的命令式編程語言(imperative programming language)相關的知識和經驗反而成了負擔,若能拋掉以往固有的觀念轉以全新的視角來看待這些新奇東西,仿佛會更好接受些,真是莫名其妙。
Bartosz Milewski 在其博客上寫了不少 Haskell 及函數式編程相關的文章,讀來真是受益良多,這位大哥很多年前就開始探討 c++ 模板元編程與 Haskell 編程之間所存在的不是那么直接卻又確乎存在的微妙聯系,許多觀點讓人眼前一亮乃至發人深思,比如說從范疇論的角度來理解和解釋什么是單子(monad)(接下來准備寫篇博客總結一下),從函數式編程的角度來看待和進行 C++ 模板編程等,觀點十分有見地且讓人啟發。Bartosz 講 Haskell 喜歡從數學的角度來闡述,視角和格局非同一般,當然他不是第一位這樣做的,事實上 Haskell 與數學本來就有着許多不得不說卻又說不清道不明的曖昧關系(住口!)。
范疇論基本概念
如果你是第一次聽說范疇論(category theory),看到這高大上的名字估計心里就會一咯噔,到底數學威力巨大,光是高等數學就能讓很多人噩夢連連。和搞編程的一樣,數學家喜歡將問題不斷加以抽象從而將本質問題抽取出來加以論證解決,范疇論就是這樣一門以抽象的方法來處理數學概念的學科,主要用於研究一些數學結構之間的關系及聯系。
在范疇論里,一個范疇(category)指的是這樣一個好東西,它由三部分組成:
- 一系列的對象(object).
- 一系列的態射(morphism).
- 一個組合(composition)操作符,用點(.)表示,用於將態射進行組合。
一個對象可以看成是一類東西,數學上的群,環,甚至簡單的有理數,無理數等都可以歸為一個對象,對應到編程語言里,可以理解為一個類型,比如說整型,布爾型,類型事實上可以看成是值的集合,例如整型就是由 0,1,2...等組成的,因此范疇論里的對象簡單理解就可以看成是值(value)的集合。
一個態射指的是一種映射關系,簡單理解,態射的作用就是把一個對象 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 里,函數也是通過點號(.)進行組合的。
另外三個約束條件很容易證明也是滿足,因此整個 Haskell 從數學的角度上看它就是一個范疇,這個角度的理解是很深刻的,這樣一來傳統意義上諸如語法,類型,函數等語言特性其實都只是這個內在本質的外在表現而已。
函子
前面對范疇的介紹反映了范疇內部各個對象之間的聯系與相互作用,在范疇論里另外研究的重點是范疇與范疇之間的關系,就正如對象與對象之間有態射一樣,范疇與范疇之間也存在某些映射,從而可以將一個范疇映射為另一個范疇,這種映射在范疇論中叫作函子(functor),具體來說,對於給定的兩個范疇 A 和 B, 函子的作用有兩個:
-
將范疇 A 中的對象映射到范疇 B 中的對象。
-
將范疇 A 中的態射映射到范疇 B 中的態射。
顯然,函子反映了不同的范疇之間的內在聯系,函子的定義是十分松散的,而不同范疇之間的關系有強有弱,一個隨便定義的函子很多時候並不能太深刻反映范疇之間結構上的聯系,因此數學上,對函子通常有幾個限制,先假設 F 是范疇 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 中,類型間的關系並不是普遍存在的,比如說, Int -> Bool 就沒有直接對應的映射關系,而存在映射關系的類型,它們都有一些共同的特點,比如可以看成是簡單類型與復雜類型之間的相互轉換。
type constructor 就是自函子的一部分!
好了,現在類型到類型的映射在 Haskell 中找到了,那態射到態射之間的映射呢?必竟這也是函子的必要組成部分。
在 Haskell 中,態射就是一般的函數,把一個函數映射為另一個函數,聽起來不就是高階函數在干的事情嘛。具體來說,映射函數這件事可以認為來自 Functor 這個 typeclass,連名字都一模一樣,目的昭然若揭。Haskell 中的 Functor 是一個 typeclass,它的定義如下:
class Functor f where
fmap:: (a -> b) -> f a -> f b
fmap 干嘛的?顯然就是用來把態射 (a -> b)
映射為態射 (f a -> f b)
的,它把范疇里的態射映射到另一個態射,且遵守了函子在映射態射時所需要遵守的兩個原則。
講到這里,我們一步一步不知不覺就已經向着 monad 靠近了,好激動,先打住了,回頭再整理整理。
【參考】
http://en.wikibooks.org/wiki/Haskell/Category_theory
http://bartoszmilewski.com/2011/01/09/monads-for-the-curious-programmer-part-1/