背景
所有一切的開始都是因?yàn)檫@句話:一個(gè)單子(Monad)說白了不過就是自函子范疇上的一個(gè)幺半群而已,有什么難以理解的。第一次看到這句話是在這篇文章:程序語言簡史(偽)。這句話出自Haskell大神Philip Wadler,也是他提議把Monad引入Haskell。Monad是編程領(lǐng)域比較難理解的概念之一,大部分人都是聞"虎"而色變,更不用說把它"收入囊中"了。我曾經(jīng)好幾次嘗試去學(xué)習(xí)Monad,F(xiàn)unctor等這些范疇論里的概念,最終都因?yàn)樗y理解,半途而廢。
這次的開始完全是個(gè)誤會。幾周之前我開啟了重溫Scala的計(jì)劃。當(dāng)我看到Scala類型系統(tǒng)和Implicit相關(guān)章節(jié)時(shí),遇到了Scala中比較重要的設(shè)計(jì)模式:類型類(type class)。于是想找一個(gè)大量使用了type class模式的開源類庫學(xué)習(xí)其源碼,以加深理解type class模式。Scalaz是個(gè)不錯(cuò)的選擇。但是有一個(gè)問題,Scalaz是一個(gè)純函數(shù)式的類庫,學(xué)習(xí)它必然又會遇到Monad那些概念。好吧,再給自己一次機(jī)會。
概念篇
我們分析一下Philip這句話:一個(gè)單子(Monad)說白了不過就是自函子范疇上的一個(gè)幺半群而已。這句話涉及到了幾個(gè)概念:單子(Monad),自函子(Endo-Functor),幺半群(Monoid),范疇(category)。首先,我們先來把這些概念搞清楚。
范疇
范疇的定義
范疇由三部分組成:
- 一組對象。
- 一組態(tài)射(morphisms)。每個(gè)態(tài)射會綁定兩個(gè)對象,假如f是從源對象A到目標(biāo)對象B的態(tài)射,記作:
f:A -> B
。 - 態(tài)射組合。假如h是態(tài)射f和g的組合,記作:
h = g o f
。
下圖展示了一個(gè)簡單的范疇,該范疇由對象 A, B 和 C 組成,有三個(gè)單位態(tài)射 id_A, id_B 和 id_C ,還有另外兩個(gè)態(tài)射 f : C => B 和 g : A => B 。
態(tài)射我們可以簡單的理解為函數(shù),假如在某范疇中存在一個(gè)態(tài)射,它可以把范疇中一個(gè)Int對象轉(zhuǎn)化為String對象。在Scala中我們可以這樣定義這個(gè)態(tài)射:f : Int => String = ...
。所以態(tài)射的組合也就是函數(shù)的組合,見代碼:
scala> val f1: Int => Int = i => i + 1
f1: Int => Int = <function1>
scala> val f2: Int => Int = i => i + 2
f2: Int => Int = <function1>
scala> val f3 = f1 compose f2
f3: Int => Int = <function1>
范疇公理
范疇需要滿足以下三個(gè)公理。
態(tài)射的組合操作要滿足結(jié)合律。記作:
f o (g o h) = (f o g) o h
對任何一個(gè)范疇 C,其中任何一個(gè)對象A一定存在一個(gè)單位態(tài)射,
id_A: A => A
。并且對于態(tài)射g:A => B 有id_B o g = g = g o id_A
。態(tài)射在組合操作下是閉合的。所以如果存在態(tài)射
f: A => B
和g: B => C
,那么范疇中必定存在態(tài)射h: A => C
使得h = g o f
。
以下面這個(gè)范疇為例:
f 和 g 都是態(tài)射,所以我們一定能夠?qū)λ鼈冞M(jìn)行組合并得到范疇中的另一個(gè)態(tài)射。那么哪一個(gè)是態(tài)射 f o g 呢?唯一的選擇就是 id_A 了。類似地,g o f=id_B 。
函子
函子定義
函子有一種能力,把兩個(gè)范疇關(guān)聯(lián)在一起。函子本質(zhì)上是范疇之間的轉(zhuǎn)換。比如對于范疇 C 和 D ,函子F : C => D
能夠:將 C 中任意對象a 轉(zhuǎn)換為 D 中的 F(A); 將 C 中的態(tài)射f : A => B
轉(zhuǎn)換為 D 中的 F(f) : F(A) => F(B)
下圖表示從范疇C到范疇D的函子。圖中的文字描述了對象 A 和 B 被轉(zhuǎn)換到了范疇 D 中同一個(gè)對象,因此,態(tài)射 g 就被轉(zhuǎn)換成了一個(gè)源對象和目標(biāo)對象相同的態(tài)射(不一定是單位態(tài)射),而且 id_A 和 id_B 變成了相同的態(tài)射。對象之間的轉(zhuǎn)換是用淺黃色的虛線箭頭表示,態(tài)射之間的轉(zhuǎn)換是用藍(lán)紫色的箭頭表示。
單位函子
每一個(gè)范疇C都可以定義一個(gè)單位函子:Id: C => C
。它將對象和態(tài)射直接轉(zhuǎn)換成它們自己:Id[A] = A; f: A => B, Id[f] = f
。
函子公理
- 給定一個(gè)對象 A 上的單位態(tài)射Id_A , F(Id_A) 必須也是 F(A) 上的單位態(tài)射,也就是說:F(Id_A) = Id_(F(A))
- 函子在態(tài)射組合上必須滿足分配律,也就是說:F(f o g) = F(f) o F(g)
自函子
自函子是一類比較特殊的函子,它是一種將范疇映射到自身的函子 (A functor that maps a category to itself)。
函子這部分定義都很簡單,但是理解起來會相對困難一些。如果范疇是一級抽象,那么函子就是二級抽象。起初我看函子的概念時(shí),由于其定義簡單,并且我很熟悉map這種操作,所以一帶而過。當(dāng)看到Monad時(shí),發(fā)現(xiàn)了一些矛盾的地方。返回頭再看,當(dāng)初的理解是錯(cuò)誤的。所以,在學(xué)習(xí)這部分概念時(shí),個(gè)人有一些建議:1. 函子是最基本,也是最重要的概念,這個(gè)要首先弄明白。本文后半部分有其代碼實(shí)現(xiàn),結(jié)合代碼去理解。如何衡量已經(jīng)明白其概念呢?腦補(bǔ)map的工作過程+自己實(shí)現(xiàn)Functor。2. 自函子也是我好長時(shí)間沒有弄明白的概念。理解這個(gè)概念,可以參看Haskell關(guān)于Hask的定義。然后類比到Scala,這樣會容易一些。
群
下邊簡單介紹群相關(guān)的概念。相比函子、范疇,群是相對容易理解的。
群的定義
群表示一個(gè)擁有滿足封閉性、結(jié)合律、有單位元、有逆元的二元運(yùn)算的代數(shù)結(jié)構(gòu)。我們用G表示群,a,b是群中元素,則群可以這樣表示:
- 封閉性(Closure):對于任意a,b∈G,有a*b∈G
- 結(jié)合律(Associativity):對于任意a,b,c∈G,有(a\b)\c=a\(b\c)
- 單位元或幺元 (Identity):存在幺元e,使得對于任意a∈G,e\a=a\e=a
- 逆元:對于任意a∈G,存在逆元a-1,使得a-1\a=a\a^-1=e
半群和幺半群
半群和幺半群都是群的子集。只滿足封閉性和結(jié)合律的群稱為半群(SemiGroup);滿足封閉性,結(jié)合律同時(shí)又有一個(gè)單位元,則該群群稱為幺半群。
概念到此全部介紹完畢。數(shù)學(xué)概念定義通常都很簡單,一句兩句話搞定,但是由于其抽象程度高,往往很難理解。下邊我們將通過Scala來實(shí)現(xiàn)其中的一些概念。
Scala和范疇論
大談了半天理論,回到編程中來。對程序員來說,離開代碼理解這些定義是困難的,沒有實(shí)際意義的。
群的代碼表示
由于實(shí)際應(yīng)用中不會涉及到群,所以我們來看半群的代碼表示。從上邊的概念我們知道,半群是一組對象的集合,滿應(yīng)足封閉性和結(jié)合性。代碼如下:
trait SemiGroup[A] {
def op(a1: A, a2: A): A
}
A表示群的構(gòu)成對象,op表示兩個(gè)對象的結(jié)合,它的封閉性由抽象類型A保證。接著來看Monoid的定義,Monoid是SemiGroup的子集,并且存在一個(gè)幺元。代碼如下:
trait Monoid[A] extends SemiGroup[A]{
def zero: A
}
下邊給出了三個(gè)例子,分別是string、list和option的幺半群實(shí)現(xiàn)。對于不同的幺半群群,它們的結(jié)合行為,和幺元是不一樣的。當(dāng)自己實(shí)現(xiàn)一個(gè)群時(shí)一定要注意這點(diǎn)。比如對于Int的幺半群,在加法和乘法的情況下幺元分別是0和1。
val stringMonoid = new Monoid[String] {
def op(a1: String, a2: String) = a1 + a2
def zero = ""
}
def listMonoid[A] = new Monoid[List[A]] {
def op(a1: List[A], a2: List[A]) = a1 ++ a2
def zero = Nil
}
def optionMonoid[A] = new Monoid[Option[A]] {
def op(a1: Option[A], a2: Option[A]) = a1 orElse a2
def zero = None
}
Functor的代碼表示
trait Functor[F[_]] {
def map[A, B](a: F[A])(f: A => B): F[B]
}
//list Functor的實(shí)現(xiàn)
def listFunctor = new Functor[List] {
def map[A, B](a: List[A])(f: (A) => B) = a.map(f)
}
Functor代碼是很簡單的,但是,也不是特別容易理解(和其概念一樣)。我在理解這段代碼的時(shí)候又遇到了問題。第一個(gè)問題:A -> F[A]這個(gè)映射在哪里?第二個(gè)問題:A => B => F[A] => F[B]這個(gè)映射又體現(xiàn)在哪里?以下是我的理解:
- Functor的定義帶有一個(gè)高階類型F[ \_ ]。在Scala里,像List[T],Option[T],Either[A, B]等這些高階類型在實(shí)例化時(shí)必須要確定類型參數(shù)(把T,A,B這些類型稱為類型參數(shù))。所以,A->F[A]這條映射產(chǎn)生在F[ \_ ]類型實(shí)例化的時(shí)候。List[Int]隱含了這樣一條映射:Int => List[Int]。
- 要理解這個(gè)映射關(guān)系:A => B => F[A] => F[B],首先來看listFunctor.map的使用。
map[Int, Int](List(1, 2, 3))(_ + 1)
,對于map它的入?yún)⑹?code>List(1, 2, 3),執(zhí)行過程是List中的每一個(gè)元素被映射該函數(shù)_: Int + 1
,得到的結(jié)果List(2, 3, 4)。所以,對于List這個(gè)范疇來說,這個(gè)過程其實(shí)就是:List[Int] => List[Int]。放眼到Int和List范疇,就是Int => Int => List[Int] => List[Int]
Monad
OK,該介紹的背景知識都說的差不多了。我們接下來看Monad。Monad的定義是這樣的:Monad(單子)是從一類范疇映射到其自身的函子(天吶,和自函子的定義一模一樣啊)。我們來看詳細(xì)的定義:
Monad是一個(gè)函子:M: C -> C,并且對C中的每一個(gè)對象x以下兩個(gè)態(tài)射:
- unit: x -> M[x]
- join/bind: M[M[x]] -> M[x]
第一個(gè)態(tài)射非常容易理解,但是第二個(gè)是什么意思呢?在解釋它之前我們先來看一個(gè)例子:
scala> val s = Some(1) //1
s: Some[Int] = Some(1)
scala> val ss = s.map(i => Some(i + 1)) //2
ss: Option[Some[Int]] = Some(Some(2))
scala> ss.flatten //3
res6: Option[Int] = Some(2)
scala> val sf = s.flatMap(i => Some(i + 1)) //4
sf: Option[Int] = Some(2)
程序第二步,把Monad當(dāng)做一個(gè)普通的函子執(zhí)行map操作,我們得到了Some(Some(2)),然后執(zhí)行flatten操作,得到了最終的Some(2)。也就是說,join就是map + flatten。接著看第四步,flatMap一次操作我們就得到了期望的結(jié)果。join其實(shí)就是flatMap。
接下來我們用Scala實(shí)現(xiàn)Monad的定義:
trait Monad[M[_]] {
def unit[A](a: A): M[A] //identity
def join[A](mma: M[M[A]]): M[A]
}
還有一種更為常見的定義方式,在Scala中Monad也是以這種方式出現(xiàn):
trait Monad[M[_]] {
def unit[A](a: A): M[A]
def flatMap[A, B](fa: M[A])(f: A => M[B]): M[B]
}
其實(shí)這兩種定義方式是等價(jià)的,join方法是可以通過flatMap推導(dǎo)出來的:def join[A](mma: M[M[A]]): M[A] = flatMap(mma)(ma => ma)
結(jié)尾
不知道大家對Monad的概念有沒有一個(gè)大概的了解了?其實(shí)它就是一個(gè)自函子。所以,當(dāng)理解了函子的概念時(shí),Monad已經(jīng)掌握了百分之八九十。剩下的百分之十就是不斷的練習(xí)和強(qiáng)化了。
那我們再回到Philip的這句話:一個(gè)單子(Monad)說白了不過就是自函子范疇上的一個(gè)幺半群而已
。該如何理解這句話?我就不再費(fèi)勁去解釋了,如果上邊的概念都弄明白了,這句話自然也就明白了。另外,受限于個(gè)人的能力,及語言表達(dá)水平,文中難免有錯(cuò)誤。為不影響大家追求真理,給出我學(xué)習(xí)時(shí)所參看的一些資源。
參看文檔:
《Functional programming in scala》
http://stackoverflow.com/questions/3870088/a-monad-is-just-a-monoid-in-the-category-of-endofunctors-whats-the-problem
http://www.zhihu.com/question/24972880
http://jiyinyiyong.github.io/monads-in-pictures/
http://hongjiang.info/scala/
http://yi-programmer.com/2010-04-06_haskell_and_category_translate.html#id5
http://www.jdon.com/idea/monad.html