怎樣用簡單的語言解釋 monad?


使用 join 詮釋的話,Monad 會有一個非常不同的理解:Monad 是可以增(return/unit,mathrm{Id}
ightarrow T)減(join,T^2
ightarrow T)層數的「箱子」。而 unit 和 join 因為滿足某些定律所以形成了一個幺半群(對,這就是那老梗)。所以,Monad 代表的是層次,而不是順序。(回想下 CPS,是不是用層次表示順序的?)

Haskell 的 bind 可以看作 fmap 和 join 的複合

首先解釋 Endofunctor,Endofunctor 是(不加限制的)箱子,它把類型(一個類型範疇里的對象)T 裝進去變成 F(T),同時還能保證所有函數(態射)A-&>B,都有一個帶箱子的版本 F(A)-&>F(B)。

而 Monad 呢?就是除了前面的兩個箱子,我們還能定義出把任意類型的數值裝進箱子的 unit:T-&>F(T),以及把兩層箱子只留一層的 join:F(F(T))-&>F(T)。

Endofunctor 們可以組成一個新的範疇,其中有個很簡單的元素 Id,它代表的「箱子」就是「沒箱子」,恆等變換。由於恆等變換 Id 存在,所以我們可以嘗試著用「二階」的手段去掉 unit 和 join 簽名裡面的 T,只關心 F 的部分,於是我們有了:

  • Unit : Id -&> F
  • Join : F × F -&> F

這個形式和幺半群很相似,所以我們說:A Monad is a monoid object in a category of endofunctors。

--

  • 對於數組,箱子就是數組本身,join 定義成 concat,把兩層數組合併成一層
  • 對於 Maybe,箱子是 Nothing/Just 包圍,join 定義成把整套結構排扁
  • 對於「副作用」,箱子是對某種狀態的修改(用函數的形式表示),join 定義成副作用的複合
  • 對於 Cont,箱子是形如lambda k.,k,x的一層封裝(接受一個 continuation,把「裡面的東西」丟給他;數學上是一個雙對偶空間),join 就定義為mathrm{join} f k=f,(lambda f=(lambda k_2. k_2 (lambda k_1. k_1 x))(lambda f=(lambda f=(lambda k_1. k_1 x),k=k x

  • Free monad 就是構造間隔的紅綠兩層盒子,在 join 那一步抽掉綠色的盒子(Free F),只留下紅色的(F),然後根據你就可以自己決定怎麼組合它們……


數學的定義其它答案都解釋得很多了。這裡,我提供一種從具體例子到抽象的解釋。希望能幫 monad 除掉「the m-word」的名聲。

1 為什麼說列表是 Monad?

問題標籤里有 Scala,這裡我們就先來考察 Scala 里的 Seq 是否是 monad。

我們來看看 Seq 能幹什麼:

首先,我們可以用一個元素構造 Seq:

val persons = Seq(john)

注意這裡的構造指:根據一個元素構造出僅含一個元素的 Seq。

第二,我們還可以在一個 Seq 對象上 flatMap(這裡順便提供一個 關於 flatMap 的直觀解釋)

persons.flatMap(p =&> p.favoriteBooks)

Monad 就是對這兩個行為的抽象。我們分別稱呼上面兩個函數為 idflatMap(這裡我們不使用 return 和 bind 這兩個不直觀的名字)。

2 Monad 有什麼好?

這樣的抽象到底有什麼好處?Monad 在數學上的優美這裡不重複,請參考其它答案。這裡只講與程序員最相關的一個優點。在一些編程語言里,比如 Scala,Monad 是有專用語法糖的。我們以一個實際例子來說明:

假設你有一個語料庫,它由不同的文檔(類型為 Document)構成,每篇 Document 又有若干句子(類型為 Sentence),每個句子又由詞構成(類型為 Word)。即,語料是由 Seq[Document] 組成,Document 里的 sentences 方法返回一個 Seq[Sentence],Sentence 里的 words 方法返回 Seq[Word]。現在你需要獲取語料中每個長度大於 4 個字母的詞的首字母。

傳統的 for 嵌套寫法這裡就不提了。普遍的寫法是利用 flatMap 和 map:

val lengths =
documents.flatMap(d =&>
d.sentences.flatMap(s =&> s.words.filter(w =&> w.length &> 4)))
.map(w =&> w(0))

然而這看著依然很亂。有沒有更優雅的寫法?有!Scala 語言里可以這樣:

for {
d &<- documents s &<- d.sentences w &<- s.words if w.length &> 4
} yield w(0)

凡是定義有 flatMap 和 map 的類,都可以在 Scala 里這麼寫。這不是一件激動人心的事情嗎?

3 到底 Monad 是什麼?

我們可以通過類比來試著猜一下,一個 monad 的 id 和 flatMap 分別具有怎樣的參數和返回值。

在 Seq 的例子中,id 就是根據一個元素構造出 Seq 的函數,即

def id[X](x: X): Seq[X] = Seq(x)

如果我們把 Seq 的 flatMap 改寫成一個全局函數,它會是這樣

def flatMap[X, Y](xs: Seq[X], f: X =&> Seq[Y]): Seq[Y] = xs.flatMap(f)

好,現在我們來抽象一下這兩個函數。假設現在有一種抽象的、行為類似 Seq 的類型,叫 M。在它上面我們可以類比 Seq,定義出抽象的 id 和 flatMap:

def id[X](x: X): M[X]
def flatMap[X, Y](xs: M[X], f: X =&> M[Y]): M[Y]

看,與具體的 Seq 的 id 和 flatMap 沒有太大區別。事實上,列表就是最天然的 monad.

有了這兩個函數的確切簽名,我們就可以給出 monad 的定義了:

能定義出上面兩個函數的類型 M 就是 monad。

當然,這兩個函數還必須滿足一些 monad 公理 &這裡暫不關心&(見文末更新)。然而類型系統無法驗證這些性質,實際使用中都是程序員自覺。

具體到代碼上,我們可以用下面這個 typeclass 來定義:

trait Monad[M[_]] {
def id[X](x: X): M[X]
def flatMap[X, Y](xs: M[X], f: X =&> M[Y]): M[Y]
}

4 還有哪些 Monad 的實例?

有了這個定義,我們會瞬間發現,其實 Monad 無處不在。除了像 Seq 那樣的列表類型,這裡再多考察幾個其它類型的。

4.1 Option Monad

我們來看看許多函數式語言里都有的 Option 是否是 monad。方便起見,這裡仍然用 Scala 語言。我們試試能不能定義出 Option 的 id 和 flatMap:

def id[X](x: X): Option[X] = Some(x)
def flatMap[X, Y](ox: Option[X], f: X =&> Option[Y]): Option[Y] =
os.flatMap(f)

看,我們成功定義出了這兩個函數。所以 Option 是 monad。我們說明這個這又有什麼用?

來,我們看看,究竟什麼時候需要 Option?有返回 null 的需求,但不想看到 NullPointerException 時。比如 Java 代碼中經常出現下面這種代碼:

if (person.bestFriend != null) {
if (person.bestFriend.favoriteBook != null) {
return /* anything */
}
else return null
}
else return null

為了拯救這種代碼,我們需要引入 Option。本來 person.bestFriend 返回的要麼是一個 Person 對象,要麼是一個邪惡的 null。我們可以將其返回類型改為 Option[Person]。關於 Option 是什麼,請參考任意有 Option 的標準庫文檔。

有了 Option 是 monad 的證據,我們就可以寫出如下的代碼了:

for {
bf &<- person.bestFriend fb &<- bf.favoriteBook } yield /* whatever */

看,既不會導致運行時 NullPointer 錯誤,又不用手動檢查 null,代碼又易讀。

4.2 分布 Monad

為了深化對 monad 的理解,我們再來喪病地考察分布(Distribution),看看它是不是個 monad。

我們明確一下 Distribution 到底是個什麼東西。這裡用 Scala 的 trait 定義:

trait Distribution[+X] { outer =&>
def sample: X
def flatMap[Y](f: X =&> Distribution[Y]): Distribution[Y] =
new Distribution[Y] {
def sample: Y = f(outer.sample).sample
}
}

有了這個定義,我們很容易就能寫出 id 和 flatMap 的 Distribution 版本:

我們先寫出 flatMap。直接使用 Distribution 里的 flatMap 即可:

def flatMap[X, Y](xs: Distribution[X], f: X =&> Distribution[Y]) =
xs.flatMap(f)

然後是 id。函數 id 的作用就是:接收一個樣本,構造出一個分布。那麼一個樣本能構造出來什麼分布呢?答案就是抽樣時永遠返回同一個樣本的分布:

def id[X](x: X): Distribution[X] = new Distribution {
def sample: T = x
}

因此,我們意識到,分布也是 monad!

我們來定義兩個常用的分布:Uniform 和 Gaussian:

case class Uniform(a: Double, b: Double) extends Distribution[Double] {
def sample = scala.util.Random.nextDouble() * (b - a) + a
}

case class Gaussian(μ: Double, σ2: Double) extends Distribution[Double] {
def sample = scala.util.Random.nextGaussian() * math.sqrt(σ2) + μ
}

哈哈,展示 monad 的威力的時候到了。我們可以非常自然地用現有分布定義一個新分布:

val d = for {
x &<- Uniform(0, 2) y &<- Gaussian(x, 1) } yield x + y

就像寫數學公式一樣自然。

Monad 為我們帶來了類型安全,能讓我們的程序易讀。請不要以「the m-word」稱呼它。

------- UPDATE --------

還是把 Monad 對 id 和 flatMap 的要求(即 Monad 公理)寫出來吧:

第一條:id 是 flatMap 的左單位元

flatMap(id(x), f) 等於 f(x)

第二條:id 是 flatMap 的右單位元

flatMap(xs, id) 等於 xs

第三條:結合律

flatMap(flatMap(m, f), g) 等於 flatMap(m, x =&> flatMap(f(x), g))

當然,寫成面向對象的形式可能更好理解:

id(x).flatMap(f) 等於 f(x)
xs.flatMap(id) 等於 xs
xs.flatMap(f).flatMap(g) 等於 xs.flatMap(x =&> f(x).flatMap(g))


Monad 的簡單類比通常是不全面甚至錯誤的,尤其是認為 Monad 和 IO、順序執行、副作用、命令式等必然相關。這裡我引用一下 What I Wish I Knew When Learning Haskell 2.2 ( Stephen Diehl )

Monadic Myths

The following are all false:

  • Monads are impure.
  • Monads are about effects.
  • Monads are about state.
  • Monads are about imperative sequencing.
  • Monads are about IO.
  • Monads are dependent on laziness.
  • Monads are a "back-door" in the language to perform side-effects.
  • Monads are an embedded imperative language inside Haskell.
  • Monads require knowing abstract mathematics.


這裡有個用物理系統來類比的解釋 http://www.haskell.org/haskellwiki/All_About_Monads#A_physical_analogy_for_monads 如果對數學概念不了解可以看這個。


理解Monad還真繞不開數學的範疇論,只有從範疇論來解釋Monad才是最好的。而從範疇論來解釋Monad其實就是自函子範疇上的一個幺半群而已,非常簡潔明了。具體的見我更詳細些的解釋如何解釋 Haskell 中的單子?。

我之前也一直試圖不用數學的範疇論來解釋,如狀態、context、序列操作等,但總感覺蒙了一層紗,總覺得有些地方就是不清晰。不得已回到數學上來,看了範疇論相關的書和wiki百科上關於Functor、Natural Transform、Monad的解釋,總算明白了「Monad其實就是自函子範疇上的一個幺半群而已」這句話。

如果結合Haskell語言來理解Monad則最好從return、join的形式來看,不要從return、bind的形式來看,容易誤入歧途,很有可能將Monad理解為由序列操作構造的結構、隱藏副作用等。

@趙丙峰,你說的「Monad是用來表達序列執行的一種構造」其實是Free Monad的典型應用,譬如我們經常用到的IO Monad其實就是一種Free Monad,將單獨的IO action蒐集在一起得到IO action的序列,通過main函數的執行完成整個IO action序列。而main函數的執行則在運行時有一個類似runIO的函數將整個IO action序列解釋為真實世界的IO操作,從而可以獲取真實世界的輸入和向真實世界輸出純函數運行的結果。而如果我們有另一個運行時,在執行main函數時不是實用runIO這樣的函數,而是一個將runIOLog這樣的將IO action解釋為單純的記錄IO命令的函數,則我們寫的IO action序列對真實世界沒有任何影響。這也就是為什麼國外有些Haskeller說Haskell的IO action也是純的。


就編程(僅限 Haskell , Scala 不懂)來說,把他理解成一種帶上下文的順序執行的結構就差不多夠用了,反正我在之前也就這麼過來了,但是嚴肅意義上是不對的,嚴肅的解釋就是那句老梗,自函子範疇上的一個幺半群。

可以說 IO a 是一個算出 a 的過程,這也是 Haskell 中 IO 的定義:

newtype IO a = IO (State# RealWorld -&> (# State# RealWorld, a #))

但是不能說 Monad 也是一個算出某種值的過程,只是 Monad 的性質恰好可以描述這一過程。

順序執行可以用 Monad 表示,但是 Monad 本身不是順序執行

副作用可以用 Monad 表示,但是 Monad 本身不是副作用

上下文相關的過程可以用 Monad 表示,但是 Monad 本身也跟上下文沒什麼關係

。。。

Monad 就是上面這些結構的共性,這個共性就是。。。自函子範疇上的一個幺半群

但是你單是理解了 Monad 幾乎是沒有任何用處的,因為你還是不知道他有什麼用,你還要知道

Monad 可以表示順序執行

Monad 可以表示副作用

Monad 可以表示上下文相關的過程

。。。

不過從現實中來看的話。。。理解了什麼是 Monad 的人鮮有不知道上面這些用途的就是了

於是我寫了一個 YAMT -&>_-&>


What Does Monad Mean? on Vimeo


我姑摸一陣, 發現為數不少的 Monad 都能當作一間工廠解釋, 不敢說是全部, 但不管是 State, IO, Maybe 都算能說的通, 其範圍貌似已符合初學者需求

Monad的兩種運算, 像是在為工廠鋪設生產線

(&>&>=) :: m a -&> (a -&> m b) -&> m b

return :: a -&> m a

m是一間工廠的標章, 每個階段生產出的東西, 都需要貼上m標章, 上面載有規格之類的訊息, 讓下一階段的機器以讀取 m 標章決定生產方式

(&>&>=) 即是說產生出 m a 後, 要接入另一個機器(a -&> m b) , 其(a -&> m b)表示說這機器吃 a 類型的成分, 並會讀取m標章決定要怎生產b, 於是便把上一階段產品 m a 的標章拔掉成 a, 並以a為成份製造出b, 並貼上另一個新的標章成為m b

return 是把外來的產品貼上標章m

下面這條Monad的限制規則也可以說得很形象

(m &>&>= f) &>&>= g

m &>&>= (x -&> f x &>&>= g)

假設有有一個產品m, 需要依序接入f機器產出東西後再送入b機器

那麼先把產品送進f再送進g, 跟先把f及g這兩台機器組裝好後才把m產品送入, 兩者的最終產品功能一致

這裡拿 State Monad 當例子

類型 State Int Bool 即是說有間工廠叫做 State, 而 State Int 就是以整數分類的無數種生產標章, 而下一個生產階段會以讀取State Int標章, 來決定下一階段要如何生產


一個單子(Monad)說白了不過就是自函子範疇上的一個幺半群而已,有什麼難以理解的。


這個必須要回答下。

要知道,為了徹底理解Haskell,我先後學習了四次,幾乎所有的Monad教程都讀過。資質是差了點,但是所有的Monad教程都不明所以才是關鍵啊。

據說每一個「徹底理解」Moand 的都抑制不住要寫一個新的YAMT,但是每一個都是自己的不同理解,說盲人摸象可能有點過,但是只涉及其中部分,甚至是特定場景下的一部分應用,無論如何也說不到解釋Monad,充其量就是一個如何應用 Monad的的例子罷了。

下面的解釋僅僅是目前的理解,自認為比所有的解釋都更直觀,但是對你有沒有就不好說了。我的解釋主要在於:

1. 不和Haskell綁定。幾乎所有的Monad解釋離不開Haskell

2. 不和I/O等所謂的副作用綁定。所有純函數式語言都不支持任何副作用,有了Monad似乎就可以了,這只是一種誤解。副作用永遠都是副作用,不可能在純函數語義中存活。Monad耍了一個花招,可以在提供等價效果的基礎上支持副作用而已。

3. 不使用數學語言。為了學習這個Monad還真的嘗試過了解範疇論,但是最終的結果是不必。這個東西不過恰好可以用用範疇論描述而已。

那麼,怎麼一句話解釋Monad呢?Monad是一種用來表達序列執行的構造。

神么?

我複述一下,Monad是用來表達序列執行的一種構造。

容我盡量解釋一下。

函數是語言的所有表達是都是環境無關的,亦即無副作用。給定參數,肯定返回唯一返回值。所有在這種場景下默認是所有表達式都可以並行執行的。Monad提供的一種手段,使的某些執行不能並行,只能序列。什麼樣的操作只能序列呢?比如單線程語義下的I/O。但是本質上任何需要序列的都可以用Monad表達。

但是,並不是所有的序列都需要用Monad來表達。在1+2*3這樣的語句中,乘必須先於加執行,否則無意義。但是幾乎所有的語言都把這種序列規則內嵌到語言本身,Haskell也是如此。

另一種命令式語言中常見的序列是函數調用之前對參數求值。這也是種隱含的序列。Haskell放鬆了此限制,所以就成了lazy語言了。完全可以通過Monad把Haskell變成非lazy的語言。

這是其中一半的解釋。

人類思維本身就是序列的,先做A,再做B,無不如此。但是從大的環境下,每個人的做法無疑是(基本)完全並行的。這很像編程語言的要解決的問題,是以序列執行為重點,擴充到並發呢?還是以並發執行為基礎,擴充到序列呢?

傳統的命令式語言應用第一種哲學,取得了巨大的成功;Haskell等函數是語言採用第二種哲學,大家期翼的大放異彩現在還沒有看到。

決定這麼做是一方面,如何做才是關鍵。

命令式語言很簡單,把識別潛在可並發的任務交與睿智或愚笨的程序員。所有我們有了線程,鎖,同步等一打以上的不同玩意兒。

函數式語言也很簡單,把識別潛在需序列化的操作的任務也交給睿智或愚笨的程序員。於是我們有了Monad以及Monad之前一打稀奇古怪的東西。

但是函數式語言,特別是Haskell的特性在於對類型這個東西的強硬支持。Haskell在類型支持上強制到保守的程度。如何用類型相容的辦法處理序列操作成一個難點,但是終於在大約15年之前取得突破。

計算機科學家向來為數學家所瞧不起,但是每個人都在刻苦學些數學以充門面。直到有一天有一個人意識到一本極度變態的數學範疇學中的某個概念可能可以解決這個問題,這個就是Monad。

下面是對於Monad的規範定義的「解釋」。

一個Monad由三部分組成,類型定義,操作綁定定義以及Monad構造定義,分別對應於規範解釋中的類型構造器,bind operator以及unit function。

這裡不由得要吐槽下,計算分多個層面的,比如符號計算,值計算,類型計算等。把這些混在一起,不區分計算的語義是最讓人頭痛的。

我把類型構造器解釋為類型定義,以及,你想用什麼樣的類型表達法來表示你要序列化的東西?

綁定操作符做好理解,如果表達綁定了兩個操作使之可以用與綁定更多的同類操作?

我把unitfunction解釋為Monad構造定義,亦即如何從一個具體的類型中構造一個可適用於此的Monad類型?

這個解釋夠直觀清晰么?

極端地講,所有的命令式語言都是Monad的不同用法。怪不得Haskell大拿說Haskell是世界上最好的命令式語言呢。

我先前承諾說不綁定到Haskell,但是現在發現通篇似乎都和Haskell相關,這是因為一個顯而易見的事實,Monad的概念以及應用應該都是從Haskell肇始的。

一般的回答都會在最後列出一堆文獻以供參考。但是我的記性實在太差,記不住太多。大家寬恕則個。

我相信這裡面的思想大部分是別人的,謹致謝意。


Monad是一種邪教。

我過幾天會寫一篇博客,講講為什麼Monad是邪教,以及怎麼調教邪教。


強類型版 Continuation Passing Style


用Scala來解釋,一個monad是帶有兩種操作flatMap和unit的類型M[T],而且必須滿足結合律,必須存在左單位元和右單位元。

trait M[T] {

def flatMap[U](f: T=&> M[U]): M[U]

}

def unit[T](x: T): M[T]

先佔坑,有空再展開。。


程序設計上的一種約定,想像成一個介面就好了

(至於純不純,要不要純,那是另一碼事了~

-----------------------------

(補充下,很多答案都在說副作用,強類型什麼的,這都是帶著Haskell的影子在看Monad的本身,Monad不是haskell發明和定義的


為什麼那麼多人在 +1 s 卻沒有人 	imes 1 s 呢?因為 1 s + 1 s = 2 s,但是 1 s 	imes 1 s = 1 s^{2},這個 1 s^{2} 的類型就不再和普通的「壽命」類型兼容了。這裡 x cdot 1 s 就是個 monad(逃


哈,這個還是要從生產中實踐,遇到問題並實際解決問題之後,體會才會深刻

你會java吧?

你會java的話,搞搞vert.x,vert.x會強制讓你用non blocking的方式寫代碼

這個時候會有callback hell問題出現,這是第一步

為了解決callback hell的問題,會使用一個叫做future的東西

這是第二步,看懂了future,你就看明白了&>&>=能夠去掉函數嵌套的作用

這個意義重大,因為這能解決callback hell的問題

你寫多幾個非同步,non-blocking的api的代碼的話,就會體會到這個痛苦了

沒有被折磨過,估計很難理解

然後呢,因為都是lambda,這就是函數嘛

lambda經常放在callback裡面作為參數用

而要在callback裡面拋出或者捕捉異常,都很噁心,你在java lambda裡面拋一個異常試試

外層捕捉起來會極為難受,除非你用kotlin的coroutine

所以你自己都會想著強制封裝

這個時候就出現了monad的第一個條件,也就是return方法

如何封裝副作用,vert.x會提供一個asyncresult這樣一個機制來封裝副作用

加上前面&>&>=的第二個條件,就做出了monad,在vert.x中就是future

所以在生產環境中使用monad是非常自然而然的事

但是你沒有遇到這些問題,沒有被這些問題折磨過

你就會覺得,嗯,這個東西可能太theoretical

參考:Java(Vert.x)中的Monad(Future)


我可是簽了協議不在網上發表 monad 教程的(


轉一個reddit上的回答,我個人認為非常實用: Teaching Monads to Rubyists (without analogies) : ruby


用類型系統語言描述的「約束關係的建立與變換」。

用 C++ 的語言:id 是 構造器,flatMap 是成員方法。當然,通常意義上的flatMap是可以自由增加「變換關係」的。


感覺cats的文檔寫的很簡單明了

Cats Documentation


推薦閱讀:

函數式編程中cps(continuation-passing style )是什麼意思?
函數式語言的缺陷是什麼?
為什麼 pattern matching 常常出現在函數式編程語言?
python協程是什麼?
為什麼會有函數式編程?

TAG:Scala | 函數式編程 | Haskell | 類型系統 | 範疇論 |