Scala和範疇論 -- 對Monad的一點認識

背景

全部一切的開始都是由於這句話:一個單子(Monad)說白了不過就是自函子範疇上的一個幺半羣而已,有什麼難以理解的。第一次看到這句話是在這篇文章:程序語言簡史(僞)。這句話出自Haskell大神Philip Wadler,也是他提議把Monad引入Haskell。Monad是編程領域比較難理解的概念之一,大部分人都是聞"虎"而色變,更不用說把它"收入囊中"了。我曾經好幾回嘗試去學習Monad,Functor等這些範疇論裏的概念,最終都由於它太難理解,半途而廢。html

此次的開始徹底是個誤會。幾周以前我開啓了重溫Scala的計劃。當我看到Scala類型系統和Implicit相關章節時,遇到了Scala中比較重要的設計模式:類型類(type class)。因而想找一個大量使用了type class模式的開源類庫學習其源碼,以加深理解type class模式。Scalaz是個不錯的選擇。可是有一個問題,Scalaz是一個純函數式的類庫,學習它必然又會遇到Monad那些概念。好吧,再給本身一次機會。git

概念篇

咱們分析一下Philip這句話:一個單子(Monad)說白了不過就是自函子範疇上的一個幺半羣而已。這句話涉及到了幾個概念:單子(Monad),自函子(Endo-Functor),幺半羣(Monoid),範疇(category)。首先,咱們先來把這些概念搞清楚。程序員

範疇

範疇的定義

範疇由三部分組成:es6

  1. 一組對象。github

  2. 一組態射(morphisms)。每一個態射會綁定兩個對象,假如f是從源對象A到目標對象B的態射,記做:f:A -> B編程

  3. 態射組合。假如h是態射f和g的組合,記做:h = g o f設計模式

下圖展現了一個簡單的範疇,該範疇由對象 A, B 和 C 組成,有三個單位態射 id_A, id_B 和 id_C ,還有另外兩個態射 f : C => B 和 g : A => B 。ide

Simple-cat.png

態射咱們能夠簡單的理解爲函數,假如在某範疇中存在一個態射,它能夠把範疇中一個Int對象轉化爲String對象。在Scala中咱們能夠這樣定義這個態射:f : Int => String = ...。因此態射的組合也就是函數的組合,見代碼:函數

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>

範疇公理

範疇須要知足如下三個公理。學習

  1. 態射的組合操做要知足結合律。記做:f o (g o h) = (f o g) o h

  2. 對任何一個範疇 C,其中任何一個對象A必定存在一個單位態射,id_A: A => A。而且對於態射g:A => B 有 id_B o g = g = g o id_A

  3. 態射在組合操做下是閉合的。因此若是存在態射 f: A => B g: B => C ,那麼範疇中一定存在態射 h: A => C 使得 h = g o f

如下面這個範疇爲例:
Composition-ex.png

f 和 g 都是態射,因此咱們必定可以對它們進行組合並獲得範疇中的另外一個態射。那麼哪個是態射 f o g 呢?惟一的選擇就是 id_B 了。相似地,g o f=id_A 。

函子

函子定義

函子有一種能力,把兩個範疇關聯在一塊兒。函子本質上是範疇之間的轉換。好比對於範疇 C 和 D ,函子 F : C => D 可以:將 C 中任意對象a 轉換爲 D 中的 F(A);  將 C 中的態射 f : A => B 轉換爲 D 中的 F(f) : F(A) => F(B)

下圖表示從範疇C到範疇D的函子。圖中的文字描述了對象 A 和 B 被轉換到了範疇 D 中同一個對象,所以,態射 g 就被轉換成了一個源對象和目標對象相同的態射(不必定是單位態射),並且 id_A 和 id_B 變成了相同的態射。對象之間的轉換是用淺黃色的虛線箭頭表示,態射之間的轉換是用藍紫色的箭頭表示。

Functor.png

單位函子

每個範疇C均可以定義一個單位函子:Id: C => C。它將對象和態射直接轉換成它們本身:Id[A] = A; f: A => B, Id[f] = f

函子公理

  1. 給定一個對象 A 上的單位態射Id_A , F(Id_A) 必須也是 F(A) 上的單位態射,也就是說:F(Id_A) = Id_(F(A))

  2. 函子在態射組合上必須知足分配律,也就是說:F(f o g) = F(f) o F(g)

自函子

自函子是一類比較特殊的函子,它是一種將範疇映射到自身的函子 (A functor that maps a category to itself)。

函子這部分定義都很簡單,可是理解起來會相對困難一些。若是範疇是一級抽象,那麼函子就是二級抽象。起初我看函子的概念時,因爲其定義簡單,而且我很熟悉map這種操做,因此一帶而過。當看到Monad時,發現了一些矛盾的地方。返回頭再看,當初的理解是錯誤的。因此,在學習這部分概念時,我的有一些建議:1. 函子是最基本,也是最重要的概念,這個要首先弄明白。本文後半部分有其代碼實現,結合代碼去理解。如何衡量已經明白其概念呢?腦補map的工做過程+本身實現Functor。2. 自函子也是我好長時間沒有弄明白的概念。理解這個概念,能夠參看Haskell關於Hask的定義。而後類比到Scala,這樣會容易一些。

下邊簡單介紹羣相關的概念。相比函子、範疇,羣是相對容易理解的。

羣的定義

羣表示一個擁有知足封閉性、結合律、有單位元、有逆元的二元運算的代數結構。咱們用G表示羣,a,b是羣中元素,則羣能夠這樣表示:

  1. 封閉性(Closure):對於任意a,b∈G,有a*b∈G

  2. 結合律(Associativity):對於任意a,b,c∈G,有(a\b)\c=a\(b\c)

  3. 單位元或幺元 (Identity):存在幺元e,使得對於任意a∈G,e\a=a\e=a

  4. 逆元:對於任意a∈G,存在逆元a^-1,使得a^-1\a=a\a^-1=e

半羣和幺半羣

半羣和幺半羣都是羣的子集。只知足封閉性和結合律的羣稱爲半羣(SemiGroup);知足封閉性,結合律同時又有一個單位元,則該羣羣稱爲幺半羣

概念到此所有介紹完畢。數學概念定義一般都很簡單,一句兩句話搞定,可是因爲其抽象程度高,每每很難理解。下邊咱們將經過Scala來實現其中的一些概念。

Scala和範疇論

大談了半天理論,回到編程中來。對程序員來講,離開代碼理解這些定義是困難的,沒有實際意義的。

羣的代碼表示

因爲實際應用中不會涉及到羣,因此咱們來看半羣的代碼表示。從上邊的概念咱們知道,半羣是一組對象的集合,滿應足封閉性和結合性。代碼以下:

trait SemiGroup[A] {
    def op(a1: A, a2: A): A    
}

A表示羣的構成對象,op表示兩個對象的結合,它的封閉性由抽象類型A保證。接着來看Monoid的定義,Monoid是SemiGroup的子集,而且存在一個幺元。代碼以下:

trait Monoid[A] extends SemiGroup[A]{
    def zero: A
}

下邊給出了三個例子,分別是string、list和option的幺半羣實現。對於不一樣的幺半羣羣,它們的結合行爲,和幺元是不同的。當本身實現一個羣時必定要注意這點。好比對於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的實現
def listFunctor = new Functor[List] {
 def map[A, B](a: List[A])(f: (A) => B) = a.map(f)
}

Functor代碼是很簡單的,可是,也不是特別容易理解(和其概念同樣)。我在理解這段代碼的時候又遇到了問題。第一個問題:A -> F[A]這個映射在哪裏?第二個問題:A => B => F[A] => F[B]這個映射又體如今哪裏?如下是個人理解:

  1. Functor的定義帶有一個高階類型F[ \ ]。在Scala裏,像List[T],Option[T],Either[A, B]等這些高階類型在實例化時必需要肯定類型參數(把T,A,B這些類型稱爲類型參數)。因此,A->F[A]這條映射產生在F[ \ ]類型實例化的時候。List[Int]隱含了這樣一條映射:Int => List[Int]。

  2. 要理解這個映射關係:A => B => F[A] => F[B],首先來看listFunctor.map的使用。map[Int, Int](List(1, 2, 3))(_ + 1),對於map它的入參是List(1, 2, 3),執行過程是List中的每個元素被映射該函數_: Int + 1,獲得的結果List(2, 3, 4)。因此,對於List這個範疇來講,這個過程其實就是:List[Int] => List[Int]。放眼到Int和List範疇,就是Int => Int => List[Int] => List[Int]

Monad

OK,該介紹的背景知識都說的差很少了。咱們接下來看Monad。Monad的定義是這樣的:Monad(單子)是從一類範疇映射到其自身的函子(天吶,和自函子的定義如出一轍啊)。咱們來看詳細的定義:

Monad是一個函子:M: C -> C,而且對C中的每個對象x如下兩個態射:

  1. unit: x -> M[x]

  2. join/bind: M[M[x]] -> M[x]

第一個態射很是容易理解,可是第二個是什麼意思呢?在解釋它以前咱們先來看一個例子:

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當作一個普通的函子執行map操做,咱們獲得了Some(Some(2)),而後執行flatten操做,獲得了最終的Some(2)。也就是說,join就是map + flatten。接着看第四步,flatMap一次操做咱們就獲得了指望的結果。join其實就是flatMap。

接下來咱們用Scala實現Monad的定義:

trait Monad[M[_]] {
 def unit[A](a: A): M[A]   //identity
 def join[A](mma: M[M[A]]): M[A]
}

還有一種更爲常見的定義方式,在Scala中Monad也是以這種方式出現:

trait Monad[M[_]] {
 def unit[A](a: A): M[A]
 def flatMap[A, B](fa: M[A])(f: A => M[B]): M[B]
}

其實這兩種定義方式是等價的,join方法是能夠經過flatMap推導出來的:def join[A](mma: M[M[A]]): M[A] = flatMap(mma)(ma => ma)

結尾

不知道你們對Monad的概念有沒有一個大概的瞭解了?其實它就是一個自函子。因此,當理解了函子的概念時,Monad已經掌握了百分之八九十。剩下的百分之十就是不斷的練習和強化了。
那咱們再回到Philip的這句話:一個單子(Monad)說白了不過就是自函子範疇上的一個幺半羣而已。該如何理解這句話?我就再也不費勁去解釋了,若是上邊的概念都弄明白了,這句話天然也就明白了。另外,受限於我的的能力,及語言表達水平,文中不免有錯誤。爲不影響你們追求真理,給出我學習時所參看的一些資源。

參看文檔:
《Functional programming in scala》
http://stackoverflow.com/ques...
http://www.zhihu.com/question...
http://jiyinyiyong.github.io/...
http://hongjiang.info/scala/
http://yi-programmer.com/2010...
http://www.jdon.com/idea/mona...    

相關文章
相關標籤/搜索