如何解釋 Haskell 中的單子(Monad)?

我就不說讓大媽理解了,能讓本科生理解就行了。

--

相似問題:什麼是 Monad (Functional Programming)?

相關問題:Haskell 裡面的 Functor 是個什麼概念?


具體一些,說類型 M&(還是用各位熟悉的符號吧)是一個關於 T 的 Monad,就是說它和一個「算出一個 T 型數據的過程」差不多。請注意 Monad 只有「算出來」這部分,如果和函數類比的話它沒有傳參這一部分。在 Haskell 這類語言中,帶有傳參、帶有返回的「計算步驟」是用一個返回 Monad 的函數表示的。

所有的 Monad 都要求定義兩個算符:

  • M& return(T x),表示一個「直接返回」的步驟
  • M& operator&>&>=(M& first, (P -&> M&) f_second),表示將兩個步驟相連,並將前一步的結果傳遞給下一步。注意這裡「第二步」不是簡單的 Monad,而是一個返回 Monad 的函數,因為 Monad 表示的「計算步驟」只包含「算出結果」,不包含「讀取參數」。

同時這些算符要滿足一些公理:

  • (return x) &>&>= f 等價於 f x
  • m &>&>= return 等價於 m
  • (m &>&>= f) &>&>= g 等價於 m &>&>= (x =&> f(x) &>&>= g)。這句話實際上就說 &>&>= 滿足結合律……

然後呢,那群程序員發現有太多的類型 M& 都可以定義出這兩個算符,甚至連數組都可以。當然,Monad 最常見的用處還是表示計算步驟,進而用來處理程序流程和副作用。


既然題主想要容易理解的回答,我猜題主可能是還在學習階段。那我就盡量用最簡單的語言解釋單子(Monad)及其應用場景。

結論:

在haskell中,Monad也只是一個有自己特殊性質的typeclass。如果想要一個類型是monad的,必須實現Monad類型類,而實現Monad typeclass,只需至少實現(&>&>=)函數即可。

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

有了這個概念,你就可以實現自己的monad了。

而什麼時候要把類型定義為functor?什麼時候定義為applicative?什麼時候定義為monad?把握以下幾個點:

functor類型解決的是如何將一個in context的value(如Just 1)輸入到普通function中(如(+2) ),並得到一個in context的value(如 Just 3)。例子:

fmap (+2) Just 1

applicative類型解決的是如何將一個in context的value(如Just 1)輸入到只返回普通value的in context的function中(如Just (+2) ),並得到一個in context的value(如Just 3)。例子:

Just (+2) &<*&> Just 1

monad類型解決的是如何將一個in context的value(如Just 3)輸入到只返回in context的value的function中(如double x = Just (x * 2) ),並得到一個in context的value(如Just 6)。例子:

Just 3 &>&>= double

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

要想理解Monad,最好先理解兩個相關的概念:Functor和Applicative。

我們通過一個例子引入:對於一個常數1,我們可以將其輸入函數 (+2):

&> (+2) 1
&> 3

很簡單,沒有任何問題。

那麼,如果常數1是在一個context里呢?比如是在Maybe類型里:

&> data Maybe a = Just a | Nothing

那麼,1會被表示成Just 1, 這時如果簡單的用

&> (+2) Just 1

就會報錯:

Non type-variable argument in the constraint: Num (a -&> Maybe a)

怎麼辦?

1. Functor

對於上面的問題,我們可以用fmap來解決。fmap可以從context里提取出value,計算,再將計算出的值放回context

&> fmap (+2) Just 1
&> Just 3

我們得到了結果Just 3.

為什麼fmap可以從context中提取value?它是怎麼做到的?規則是什麼?

我們先看fmap函數出自哪裡:

&> :t fmap
&> fmap :: Functor f =&> (a -&> b) -&> f a -&> f b

那麼Functor是什麼?

我們再去查Hoogle:

The Functor class is used for types that can be mapped over.

class Functor f where

Minimal complete definition

fmap

Methods

fmap :: (a -&> b) -&> f a -&> f b

(&<$) :: a -&> f b -&> f a

......

可以這樣理解這段定義:對於一個Functor類型的數據類型f,至少需要實現fmap。對於fmap函數,其中第一個輸入(a -&> b)是一個普通函數,比如(+2);第二個輸入f a是一個Functor,比如Just 1;最後返回一個Functor,比如Just 3

對於Functor這樣的類,我們稱之為typeclass(類型類)。typeclass有點像Java的interface:實現這個介面要實現其中的方法。,

回到我們的例子,Maybe類型其實是Functor類型的。我們可以查實現Maybe的源代碼去驗證一下:

instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)

的確如此。這就是為什麼運行fmap (+2) (Just 1)可以得到Just 3。

所以總結一下,Functor可以解決的問題是,當value被放在一個context中的時候(比如Maybe類型的Just 1),我們如何將其輸入一個普通的function運算(如(+2) ),並得到in context的輸出(如Just 3)

那麼我們進一步考慮,如果function也被放在了一個context中了怎麼辦?

2. Applicative

比如這樣的一個function:

Just (+2)

如果想將Just 1輸入,直接寫

&> Just (+2) Just 1

肯定是不行的。如何解決這個問題?

我們可以用函數&<*&>來解決。

&> Just (+2) &<*&> Just 1
&> Just 3

同樣,如果去查&<*&>來自哪裡,我們會發現它是Applicative的一個方法:

A functor with application, providing operations to

  • embed pure expressions (pure), and
  • sequence computations and combine their results (&<*&>).

class Functor f =&> Applicative f where

(&<*&>) :: f (a -&> b) -&> f a -&> f b

......

也就是說,一個Applicative類型的數據類型,必須實現&<*&>方法。這和Functor是一致的,Applicative也是一種typeclass。同樣我們可以通過Maybe類型的實現來驗證它也是Applicative類型的:

instance Applicative Maybe where
pure = Just

Just f &<*&> m = fmap f m
Nothing &<*&> _m = Nothing

Just _m1 *&> m2 = m2
Nothing *&> _m2 = Nothing

總結一下,Applicative可以解決的問題是,如果當value被放在一個context中(如1被放入Maybe類型,變為Just 1),並且function也被放在一個context中(如(+2)被放入Maybe類型,變為Just (+2))時,如何運算並得到輸出

我們注意到,&<*&>函數的value是在右邊,function是在左邊的。

那我們進一步考慮,如果我們想要value在左邊,並且函數的輸出是一個in context的值(比如Just 3這種形式)呢?

3. Monad

上面的情況,是可以用&>&>=函數(也叫bind函數)來解決的。

舉個例子,假如我們有一個double函數:

double x = Just (x * 2)

它的輸出是Maybe類型的。

那麼我們想將Just 1進行double怎麼做?用以下的代碼即可:

&> Just 1 &>&>= double
&> Just 2

我們得到了Just 2。沒錯,&>&>=函數是Monad的一個方法,其定義為:

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

而Monad的定義如下:

The Monad class defines the basic operations over a monad, a concept from a branch of mathematics known as category theory. From the perspective of a Haskell programmer, however, it is best to think of a monad as an abstract datatype of actions. Haskell"s do expressions provide a convenient syntax for writing monadic expressions.

class Applicative m =&> Monad m where

Minimal complete definition

(&>&>=)

Methods

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

(&>&>) :: forall a b. m a -&> m b -&> m b

return :: a -&> m a

fail :: String -&> m a

從定義中我們可以看出,Monad也是一個typeclass,那麼作為typeclass,一個monad類型的數據類型,必須實現其方法。Monad本身定義里寫的很清楚,「這是一個來自範疇學的概念,不過從編程角度看最好將其視作一組行為的抽象數據類型」。然而這句話到底怎麼理解?

說白了,在haskell里,一個monad就是一個參數化的類型m,以及一個必須實現的函數 (&>&>=)

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

沒了。

舉個例子,如果我們的參數化類型m是Maybe,(&>&>=) 按照

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

來實現,那麼我們得到的就是一個Maybe monad。

monad有很多好的性質,比如如果我們想連續多次做double操作,用Applicative就會有點麻煩,但是用Monad就很簡單:

&> Just 1 &>&>= double &>&>= double &>&>= double &>&>= double
&> Just 16

當然,monad能做的事情遠不止這麼一點,相關也還有其他很多概念,比如將monads結合起來的方法monad transformers,不過這就是另外一回事了。

結論:

在haskell中,Monad也只是一個有自己特殊性質的typeclass。如果想要一個類型是monad的,必須實現Monad類型類,而實現Monad typeclass,只需至少實現(&>&>=)函數即可。

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

有了這個概念,你就可以實現自己的monad了。

而什麼時候要把類型定義為functor?什麼時候定義為applicative?什麼時候定義為monad?把握以下幾個點:

functor類型解決的是如何將一個in context的value(如Just 1)輸入到普通function中(如(+2) ),並得到一個in context的value(如 Just 3)。例子:

fmap (+2) Just 1

applicative類型解決的是如何將一個in context的value(如Just 1)輸入到只返回普通value的in context的function中(如Just (+2) ),並得到一個in context的value(如Just 3)。例子:

Just (+2) &<*&> Just 1

monad類型解決的是如何將一個in context的value(如Just 3)輸入到只返回in context的value的function中(如double x = Just (x * 2) ),並得到一個in context的value(如Just 6)。例子:

Just 3 &>&>= double

回答很淺薄,甚至有很多地方是通過現象倒推原因,但希望能幫到題主理解Monad的概念。

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

在其他答主的評論區看到一個很有意思的問題:為什麼大多數教材都用Maybe類型來作為functor, applicative和monad的舉例?因為Prelude實現了Functor Maybe, Applicative Maybe和Monad Maybe,所以Maybe既是functor,又是applicative,還是monad,舉例最方便嘛。


如何解釋 Haskell 中的單子? 請看這個答案


Monad是一種數學結構,haskell中的Monad意義和數學上的意義是一樣的。簡單的說單子(Monad)就是自函子範疇上的一個幺半群。這個幺半群的態射是作用在自函子上的自然變換,其單位態射是haskell中class Monad 的return函數(這個實際上是個自然變換)。而這個幺半群的態射的組合操作(composition)則是haskell中class Monad的join函數,也是一個自然變換。

從幾何直觀的概念來說,Monad是一個自相似的幾何結構,通過自函子的作用和兩個自然變換的約束得到一層層嵌套的自相似結構。

2015年5月17日更新:

我們先從比較容易理解的Kleisli範疇(return, &>=&>)定義的Monad開始,可以比較容易的得到幺半群(Monoid)的對應描述。Monoid的單位元(id)是return,組合操作(composition)是&>=&>,具體如下:

return &>=&> f = f -- left unit law
f &>=&> return = f -- right unit law
(f &>=&> g) &>=&> h = f &>=&> (g &>=&> h) -- associativity law

上面的解釋對實際編程的使用來說基本上夠用了,但是Kleisli範疇並沒有直接的解釋一開始提到的老梗:單子(Monad)就是自函子範疇上的一個幺半群。下面我們就從範疇論的概念(return, join)定義的Monad來解釋了。

我們從Functor開始,先定義Id functor,具體如下:

newtype Id a = Id a
instance Functor Id where
fmap f (Id x) = Id (f x)

然後再定義自然變換,具體如下:

type NatHask f g = forall a. (f a) -&> (g a)

函子的複合(composition)定義如下

newtype Compose f g a = Compose { unCompose :: f (g a) }

instance (Functor f, Functor g) =&> Functor (Compose f g) where
fmap k (Compose fg) = Compose $ fmap (fmap k) fg

再定義return和join這兩個自然變換,我們就得到了用範疇論的概念定義的Monad

class Applicative m =&> Monad m where
return :: NatHask Id f
join :: NatHask (Compose f f) f

這樣我們就得到了Monad的清晰的Haskell代碼解釋,更形象的Monad的幺半群(Monoid)圖示見下圖:

這裡e就是return,m就是join,?就是Compose,滿足Monad的三個基本定律。

join . return = id -- left unit law
join . fmap return = id -- right unit law
join . fmap join = join . join -- associativity law

至此我們得到了Monad的更準確的說法:

單子(Moand)是自函子的Monoidal範疇上的一個幺半群,該Monoidal範疇的張量積(tensor product,?:M×M→M)是自函子的複合(composition),單位元是Id functor。

2014年3月10日更新:

一轉眼這個答案竟然成了第一,看來得做一些補充,才不負眾望。下面就以haskell編程語言來具體 講解Monad的概念。

Monad其實可以簡單的看成是一個Functor(函子),而且是個自函子(haskell中所有的函子都是自函子)。只不過Monad相對於其他普通Functor有更多的約束,用typeclass的繼承關係來看是Functor=&>Applicative=&>Monad(ghc 7.10將會執行這一標準,ghc 7.8將對沒有按此標準定義的Monad代碼提出警告)。

Monad相對於普通函子多出來的約束就是多了兩個自然變換(return, join),這兩個自然變換的約束確保Monad是一個自相似的幾何結構。

有三種方式來定義Monad,分別是範疇論、Kleisli範疇、haskell Monad這三種。

用範疇論的概念(return, join, fmap)定義的Monad如下:

class Applicative f =&> Monad f where
return :: a -&> f a
join :: forall a. f (f a) -&> f a

用Kleisli範疇(return, &>=&>)定義的Monad如下:

class Monad m where
(&>=&>) :: forall a b c. (a -&> m b) -&> (b -&> m c) -&> (a -&> m c)
return :: a -&> m a

用haskell monad(return, &>&>=)定義的Monad如下:

class Monad m where
(&>&>=) :: forall a b. m a -&> (a -&> m b) -&> m b
return :: a -&> m a

這三種定義是等價的,(&>=&>) 、join、&>&>=的相互定義如下:

join m = m &>&>= id
join = id &>=&> id

m &>&>= f = join (fmap f m)
(&>&>=) = flip (id &>=&>)

f &>=&> g = x -&> f x &>&>= g
f &>=&> g = join . fmap g . f


Formally, the definition of a monad is like that of a monoid M in sets.The set M of elements of the monoid is replaced by the endofunctor T:X	o X, while the cartesian product 	imes of two sets is replaced by compsites of two functors, the binary operation H:M	imes M	o M of multiplication by the transformation mu:T^2	o T and the unit (identity) element eta: 1	o Mbyeta:I_x	o T. We shall thus call eta the unit and mu the multiplication of the monad T

——Categories for the Working Mathematician, Saunders Mac Lane


我剛開始接觸的時候也是雲里霧裡,後來我發現我順序弄錯了,因此在這裡建議題主先去弄明白什麼是continuation passing style(style就是風格的意思,cps就是一種風格),再自己用cps寫幾個程序玩玩,回過頭來看monad啥的,如果不談數學的話,對編程也就那樣


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


自己手動展開幾個monad的使用,你就知道是啥了。聽別人說是不可能理解的。


這個問題經典了,我已經看過不止10篇介紹文章了,但是還是有些雲里霧裡。

monad是什麼其實挺好理解的,但是為什麼需要它,或者說3定律的意義是什麼才是難點。

http://jiyinyiyong.github.io/monads-in-pictures/

鏈接是我覺得比較直觀的一個教程,而且是翻譯好的

=================

因為翻譯作者的鏈接失效了,我給一個筆記的分享吧

Functors, Applicatives, And Monads In Pictures


一個碗就是一個 Monad。你把水倒進碗里就是 return,把一碗水倒到另一個碗里就是 &>&>=。碗的作用是什麼呢?比方說你要喝水,是不是就得拿個碗。


我也看了七八篇所謂的 monad 入門,一篇也沒看懂。

不過最易讀的一篇我認為是 http://zhuoqiang.me/what-is-monad.html


不從數學的角度來理解是不可能真正體會的。

http://www1.eafit.edu.co/asr/pubs/others/cain-screen.pdf

想要有感性認識,多讀源代碼,學會怎麼用就好了。切不可用來裝逼。


貼一個最貼近程序員的解釋,from Chapter 14 of Real World Haskell:

"the fact that it uses semicolons to separate expressions has given rise to an apt slogan: monads are a kind of 「programmable semicolon」, because the behaviours of (&>&>) and (&>&>=) are different in each monad."


這篇文章角度獨特,解釋直觀。單子 可以看做一種設計模式。

Monads Demystified


你的這種理解方式其實非常好, 不一定要從數學的層面去思考, 換一個角度也不錯


什麼狗屁 Monad, Haskell里的 Monad 太複雜了點. 沒必要學那麼深. 要理解狀態傳遞, 非同步什麼的, 看看F#, F#里就叫作計算表達式. 還是這樣叫實在點. 保你能看懂的資料 F Sharp Programming/Computation Expressions


Functors are things that can be mapped over, like lists, Maybes, trees, and such.Functors, Applicative Functors and Monoids


Monad這個詞本身什麼意思,從數學的角度解釋起來雖然很複雜。

但是,拋開它的起源和名字不談,把Monad當做一個不認識的詞,僅從程序員的角度解釋就簡單了:

Haskell中的Monad是一種語言(子語言),但其語義一定程度上是你自己定義的。

換句話說,有了Monad,你可以更方便地為你的庫定義一個DSL(Domain Specific Language)。

只不過這個DSL的語法是固定的,你能改變的是語義。

這個語法是什麼,大家都知道了。

至於語義是什麼,就是庫定義的了。可以去參考比較 自帶的IO和[ParserCombinator](Parsec - HaskellWiki)

只是對於Monad的理解有很多誤區:

1、Monad是用來管理副作用的

Monad的語法的確適合描述一些有副作用的操作。但它本身和副作用一點關係都沒有,比如 ParserCombinator 就是一個基於Monad語法的語法解析庫。

2、return這個函數的意義

return這個函數,是為了迎合c語言中函數返回值的語法才(沒骨氣地)這麼叫的。

也就是說return只有當(1)它是一個do塊的最後一行;(2)在IO、ST之類真正管理副作用的Monad里 才符合return這個詞的含義。(此時代碼看起來很像一個函數返回一個值)

千萬不要被誤導。

我個人的觀點是,自FTP以後,return應該被deprecated,統統改用pure


推薦閱讀:

如何學習 Haskell ?
什麼是GADT?它有什麼作用?
OCaml or Haskell?
Comonad有什麼實際用途?
該如何理解Monad?

TAG:編程語言 | 函數式編程 | Haskell |