Monads是什么
知乎里有關于什么是Monad的問題討論,而在維基百科中也有關于Monad的釋義。作為初次接觸到Monads概念,難免會有些暈頭轉向,也難免會有些畏懼(因為Monads和數學中的范疇論有密切關系),但是Monads又是如此的重要,因為它在函數式編程中實在是應用太廣泛了,并且在Scala的標準庫中又常常遇到,使得我們不得不好好研究一番。
Monoids
Monoids是一種元素的集合,它需要滿足結合律和幺元(Identity,也稱為單位元,這種元和其他元素結合時,不會改變那么元素)這些約束條件。
比如:
- 整數類型Int,其中0是Identity,其中的任何整數滿足結合律
- 列表類型List,任何兩個列表可以通過
:::
連接起來,其中Nil或空列表[]是Identity - 字符串類型String,兩個字符串可以拼接,其中空字符串或""是Identity
Monoids在平常的編程之中無處不在,當用到一個列表,連接字符串,通過一個循環得到一個累加結果,都在使用到Monoids。
條件和定律
- 一個抽象類型A
- 一個二元結合性函數(binary associative function),對傳入的兩個A類參數進行操作后產生一個A類型結果。op操作必須是結合性的,即op(x, y) == op(y, x);op(a,op(b,c)) = op(op(a,b),c):這個定律是函數組合(function composition)不可缺的條件
- 一個恒等值(identity)。二元函數參數中如果有一個是恒等值時操作結果為另一個參數,即滿足op(identity, x) == x
示例
Monoid可以用下面的代碼描述:
trait Monoid[T] {
def op(m1: T, m2: T): T
val identity: T
}
這個特質可以被混入類型(classes)、對象(objects)或者其他特質中。請看下面的舉例:
case class StringMonoid extends Monoid[String] {
def op(s1: String, s2: String) = s1 + s2
val identity = ""
}
val stringMonoid = StringMonoid()
println(stringMonoid.op(stringMonoid.identity, "John"))
println(stringMonoid.op("John", "Hunt"))
// Output is
// John
// JohnHunt
object IntMonoid extends Monoid[Int] {
def op(x: Int, y: Int) = x + y
val identity = 0
}
println(IntMonoid.op(IntMonoid.identity, 1))
println(IntMonoid.op(1, 2))
println(IntMonoid.op(2, 1))
// Output is
// 1
// 3
// 3
Monoid和折疊
如果有一個Monoid結構和一組數據。可以通過對每個元素進行Monoid的op操作來將集合縮減為一個值,比如將一個整數列表通過元素累加的方式得到所有整數的和。
Monoid和List有著密切的聯系。在List的foldLeft操作中,用一個初始元素從列表的左邊元素開始操作,一直到對所有元素都操作完。如List("A", "B", "C").foldLeft("")(_ + _)
這個對字符串列表實現累加功能,foldLeft傳入的兩個參數分別是空字符串和二元操作運算,這正好符合Monoid的定義,可以輕松利用StringMonoid代替,List("A", "B", "C").foldLeft(StringMonoid.identity)(StringMonoid.op)
。
結合性與并行化
Monoid的結合性意味著我們在對類似List的數據結構進行折疊的時候有很大的靈活性。我們已經知道可以使用foldLeft和foldRight對一個列表進行順序的規則(reduce)操作。但是我們同樣可以將數據分成多份,并行的進行折疊,然后利用monoid將各個部分合并起來。
左折疊操作是op(op(op(a, b), c), d)
右折疊操作是op(a, op(b, op(c, d)))
并行算法為op(op(a, b), op(c, d))
,其中op(a, b)和op(c, d)是同時運算的。
如果我們對一個超大文件進行文字數統計或者尋找最大值什么的,我們可以把這個大文件分成若干小文件然后同時計算后再合計將節省很多計算時間。
Monoid模式的優缺點
優點:
- Monoid模式提供了一種在特定場景下將元素合并的標準方法
- 結合性的保證可以用來定義函數之間組合
缺點:
- 并不是所有集合都可以很容易的應用Monoid模式。比如String Monoid,不同順序的字符串進行連接可能會得到不同的結果。
從范疇論到計算機編程
從Monoid到Monad,這些概念都是從范疇論中衍生出來的。
理解范疇論的一個好方法是把它理解為應用到函數式編程領域的設計模式。范疇論定義了一些非常底層的概念抽象,這些概念可以直接用Scala這樣的支持函數式編程的語言表達。在設計軟件的時候,如果一個特定實體符合其中一個概念,那么立刻就有一整組操作可用,而且包含推理其用法的方法。
范疇是由元素對象和態射箭頭組成的,這個箭頭開始端是一個元素對象,目的地也是一個元素對象。這里態射箭頭有兩種,不同元素對象比如a和b之間的態射箭頭稱為組合箭頭,而指向自己的箭頭稱為元箭頭,或者單元,幺元。
范疇的元素對象和箭頭態射的規則如下:
- 對于箭頭f:a -> b和箭頭g:b -> c,如果有一個箭頭h: a -> c,那么就稱為它們的組合,寫法是:h=g·f
- 對于每個元素對象,都有一個單元箭頭:id:a -> a。對于任何f: a -> b,滿足f·id = f;對于任何g: c -> a,滿足id·g = g
- 組合符合結合律:f·(g·h) = (f·g)·h
態射箭頭有兩種,一種是標號1的組合箭頭,還有一種是標號2的單元箭頭。
我們將一個范疇有元素對象和態射箭頭,態射箭頭有組合和幺元兩種,且滿足結合律,這種范疇稱為Monoid。
對于某非空集合S,若存在S上的二元運算"*"使得對于任意的a,b∈S,有a*b∈S(運算封閉),則稱{S,*}為廣群。 廣群只是定義一個集合,集合中有元素和操作,操作結果也屬于這個集合,這樣泛泛的集合稱為廣群。 如果廣群再加上結合律約束,就會得到半群,因此半群是廣群的子集,要求更苛刻些,而半群如果再加上幺元(identity element)就是幺半群,也就是結合律+幺元=幺半群,所以,Monid對應的中文是幺半群。
參考資料
Functional Programming in Scala
Scala Design Patterns: Patterns for Practical Reuse and Design
什么是Monoid?
我所理解的monad(2):fold與monoid
轉載請注明作者Jason Ding及其出處
Github博客主頁(http://jasonding1354.github.io/)
GitCafe博客主頁(http://jasonding1354.gitcafe.io/)
CSDN博客(http://blog.csdn.net/jasonding1354)
簡書主頁(http://www.lxweimin.com/users/2bd9b48f6ea8/latest_articles)
Google搜索jasonding1354進入我的博客主頁