Haskell 與範疇論

twoon發表於2014-09-15

說到 Haskell,這真是一門逼格極高的程式語言,一般初學者如果沒有相關函數語言程式設計的經驗,入門直接接觸那些稀奇古怪的概念,簡直要跪下。現在回想起來,隱隱覺得初學者所擁有的指令式程式設計語言(imperative programming language)相關的知識和經驗反而成了負擔,若能拋掉以往固有的觀念轉以全新的視角來看待這些新奇東西,彷彿會更好接受些,真是莫名其妙。

Bartosz Milewski 在其部落格上寫了不少 Haskell 及函數語言程式設計相關的文章,讀來真是受益良多,這位大哥很多年前就開始探討 c++ 模板超程式設計與 Haskell 程式設計之間所存在的不是那麼直接卻又確乎存在的微妙聯絡,許多觀點讓人眼前一亮乃至發人深思,比如說從範疇論的角度來理解和解釋什麼是單子(monad)(接下來準備寫篇部落格總結一下),從函數語言程式設計的角度來看待和進行 C++ 模板程式設計等,觀點十分有見地且讓人啟發。Bartosz 講 Haskell 喜歡從數學的角度來闡述,視角和格局非同一般,當然他不是第一位這樣做的,事實上 Haskell 與數學本來就有著許多不得不說卻又說不清道不明的曖昧關係(住口!)。

範疇論基本概念

如果你是第一次聽說範疇論(category theory),看到這高大上的名字估計心裡就會一咯噔,到底數學威力巨大,光是高等數學就能讓很多人噩夢連連。和搞程式設計的一樣,數學家喜歡將問題不斷加以抽象從而將本質問題抽取出來加以論證解決,範疇論就是這樣一門以抽象的方法來處理數學概念的學科,主要用於研究一些數學結構之間的關係及聯絡。

在範疇論裡,一個範疇(category)指的是這樣一個好東西,它由三部分組成:

  1. 一系列的物件(object).
  2. 一系列的態射(morphism).
  3. 一個組合(composition)操作符,用點(.)表示,用於將態射進行組合。

一個物件可以看成是一類東西,數學上的群,環,甚至簡單的有理數,無理數等都可以歸為一個物件,對應到程式語言裡,可以理解為一個型別,比如說整型,布林型,型別事實上可以看成是值的集合,例如整型就是由 0,1,2...等組成的,因此範疇論裡的物件簡單理解就可以看成是值(value)的集合。

一個態射指的是一種對映關係,簡單理解,態射的作用就是把一個物件 A 裡的值 va 對映為 另一個物件 B 裡的值 vb,這和代數裡的對映概念是很相近的,因此也有單射,滿射等區分。態射的存在反映了物件內部的結構,這是範疇論用來研究物件的主要手法:物件內部的結構特性是通過與別的物件的關係反映出來的,動靜是相對的,範疇論通過研究關係來達到探知物件的內部結構的目的。

組合操作符的作用是將兩個態射進行組合,例如,假設存在態射 f: A -> B, g: B -> C, 則 g.f : A -> C.

看!好像沒有想象中的複雜!一個結構要想成為一個範疇, 除了必須包含上述三樣東西,它還要滿足以下三個限制:

  1. 態射要滿足結合律,即 f.(g.h) = (f.g).h。

  2. 態射在這個結構必須是封閉的,也就是,如果存在態射 f, g,則必然存在 h = f.g。

  3. 對結構中的每一個物件 A, 必須存在一個單位態射 Ia: A -> A, 對於單位態射,顯然,對任意其它態射 f, f.I = f。

講完了!範疇論就這麼點東西!-- 當然是不可能的,但暫時來說,知道這些就已經很足夠了。

Haskell 中的範疇

在 Haskell 中存在著這樣一個唯一的範疇,名字稱為 Hask, 這個 Hask 滿足前面關於範疇的全部約定,因此是範疇論裡一個純正的“範疇":

  1. 物件就是 Haskell 裡的所有型別,記得型別是一個集合。

  2. 態射就是程式語言裡的一般函式(function),如: func :: Int -> Bool,將物件 int 對映為 物件 bool。

  3. 態射的組合就是函式的組合,在 Haskell 裡,函式也是通過點號(.)進行組合的。

另外三個約束條件很容易證明也是滿足,因此整個 Haskell 從數學的角度上看它就是一個範疇,這個角度的理解是很深刻的,這樣一來傳統意義上諸如語法,型別,函式等語言特性其實都只是這個內在本質的外在表現而已。

函子

前面對範疇的介紹反映了範疇內部各個物件之間的聯絡與相互作用,在範疇論裡另外研究的重點是範疇與範疇之間的關係,就正如物件與物件之間有態射一樣,範疇與範疇之間也存在某些對映,從而可以將一個範疇對映為另一個範疇,這種對映在範疇論中叫作函子(functor),具體來說,對於給定的兩個範疇 A 和 B, 函子的作用有兩個:

  1. 將範疇 A 中的物件對映到範疇 B 中的物件。

  2. 將範疇 A 中的態射對映到範疇 B 中的態射。

顯然,函子反映了不同的範疇之間的內在聯絡,函子的定義是十分鬆散的,而不同範疇之間的關係有強有弱,一個隨便定義的函子很多時候並不能太深刻反映範疇之間結構上的聯絡,因此數學上,對函子通常有幾個限制,先假設 F 是範疇 A 與範疇 B 上一個函子,則:

  1. 對範疇 A 上的單位態射Ia, F 必須將其對映為範疇 B 上的單位態射 Ib, F(Ia) = Ib.

  2. 函子對態射的組合必須滿足分配徤,即,假設 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/

相關文章