什么是 Monad (Functional Programming)擎析?

什么是 Monad (Functional Programming)簿盅?

道生一,一生二揍魂,二生三挪鹏,三生萬物。

  • 這里的“生”愉烙,就是 “apply 函數(shù) ”(請注意,這里的 apply 動詞)解取。

  • 道步责,就是 X,一禀苦、二蔓肯、三、萬物等就是 Y振乏。當(dāng)然蔗包,這里的“一、二慧邮、三调限、萬物” 都是函數(shù)。

  • “從無開始編程” 误澳。

函數(shù)式編程的精髓就在于耻矮,我們可以用好多好多小小函數(shù),搭搭搭忆谓,組成一個個大函數(shù)裆装,最終寫出整個程序來。比如我們想寫一個函數(shù)

h: A -> C

然后手頭有兩個子函數(shù):

f: A -> B
g: B -> C

于是我們用一個膠水函數(shù)

fun compose(f,g): A->C{
    return {x->g(f(x))}
}

把那兩個小函數(shù)膠起來倡缠,做成我們要的 h 哨免。

問題:f和g合并成了h,那么可以合并的函數(shù)需要符合什么條件呢昙沦?

Monad不就是個自函子范疇上的幺半群琢唾,這有什么難理解的(A monad is just a monoid in the category of endofunctors)
—— Phillip Wadler

Monad工作原理包含兩個部分:對原范疇組合成新的范疇,這個范疇對于Monad來說必須是幺半群Monoid桅滋,可以認(rèn)為Monad是一系列自函子的組合慧耍,這種組合是一種轉(zhuǎn)換身辨,轉(zhuǎn)換的結(jié)果是Monoid。

Monad有以下特征:

Monad是一種定義將函數(shù)(函子)組合起來的結(jié)構(gòu)方式芍碧。

這些組合的方法都是符合結(jié)合律的煌珊。

而Monoid是元素對象的組合的范疇,如果這種元素對象是函數(shù)或函子(也可能是Pipe泌豆,這就復(fù)雜了去了 )定庵,那么Monad是自函子的組合范疇,Monad也是一種特殊的Monoid子集踪危。

有一個特殊幺元蔬浙,能夠和任何元素組合,導(dǎo)致的結(jié)果是不改變這些元素贞远。

函子到底是什么?

一個函子Functor是任意類型畴博,這些類型定義了如何應(yīng)用 map (fmap in Haskell) 。 也就是說蓝仲,如果我們要將普通函數(shù)應(yīng)用到一個有盒子上下文包裹的值俱病,那么我們首先需要定義一個叫Functor的數(shù)據(jù)類型,在這個數(shù)據(jù)類型中需要定義如何使用map或fmap來應(yīng)用這個普通函數(shù)袱结。這個函子Functor如下圖:

image.png

fmap的輸入?yún)?shù)是a->b函數(shù)亮隙,在我們這個案例中是(+3),然后定義一個函子Functor垢夹,這里是Haskell的Just 2溢吻,最后返回一個新的函子,在我們案例中果元,使用Haskell是Just 5促王。下圖展示了函子內(nèi)部工作原理(多了一層上下文的“盒子”封裝):

image.png

第一步是將值從上下文盒子中解救出來,然后將外部指定的函數(shù)(+3)應(yīng)用到這個值上而晒,得到一個新的值(5)硼砰,再將這個新值放入到上下文盒子中。是不是很形象生動欣硼?

Applicative

當(dāng)我們的值被一個上下文包裹题翰,就像函子Functor:

image.png

之前我們討論的是如何將一個普通函數(shù)應(yīng)用到這個函子中,現(xiàn)在如果這個普通函數(shù)也是一個被上下文包裹的:就叫 Applicative诈胜。它能知道如何應(yīng)用一個被上下文包裹的函數(shù)到一個被上下文包裹的值中豹障。

image.png

Monad

函子funtor是將一個普通函數(shù)應(yīng)用到包裹的值:


image.png

Applicative應(yīng)用一個包裹的函數(shù)到一個包裹的值:


image.png

Monad 則是將一個會返回包裹值的函數(shù)應(yīng)用到一個被包裹的值上。

image.png
image.png
image.png

那么函子焦匈、applicative和Monad三個區(qū)別是什么血公?

image.png
  • functor: 應(yīng)用一個函數(shù)到包裹的值,使用fmap/map.
  • applicative: 應(yīng)用一個包裹的函數(shù)到包裹的值缓熟。
  • monad: 應(yīng)用一個返回包裹值的函數(shù)到一個包裹的值累魔。

面對對象(OOP)可以理解為是對數(shù)據(jù)的抽象摔笤,比如把一個人抽象成一個Object,關(guān)注的是數(shù)據(jù)垦写。 函數(shù)式編程是一種過程抽象的思維吕世,就是對當(dāng)前的動作去進(jìn)行抽象,關(guān)注的是動作梯投。

image.png

名詞+動詞= 圖靈機(jī) + 函數(shù)式 =對象(狀態(tài)) + process

自函子(Endofunctor)

什么是函數(shù)(Function)命辖?
函數(shù)表達(dá)的映射關(guān)系在類型上體現(xiàn)在特定類型(proper type)之間的映射。

什么是自函數(shù)(Endofunction)分蓖?

identity :: Number -> Number

自函數(shù)就是把類型映射到自身類型尔艇。函數(shù)identity是一個自函數(shù)的特例,它接收什么參數(shù)就返回什么參數(shù)么鹤,所以入?yún)⒑头祷刂挡粌H類型一致终娃,而且值也相同。

接下來蒸甜,回答什么是自函子(Endofunctor)之前尝抖,我們先弄清什么是函子(Functor)?

函子有別于函數(shù)迅皇,函數(shù)描述的是特定類型(proper type)之間的映射,而函子描述的是范疇(category)之間的映射衙熔。

那什么是范疇(category)登颓?

我們把范疇看做一組類型及其關(guān)系態(tài)射(morphism)的集合。包括特定類型及其態(tài)射红氯,比如Int框咙、String、Int -> String痢甘;高階類型及其態(tài)射喇嘱,比如List[Int]、List[String]塞栅、List[Int] -> List[String]者铜。

接下來看看函子是如何映射兩個范疇的,見下圖:

image.png

圖中范疇C1和范疇C2之間有映射關(guān)系放椰,C1中Int映射到C2中的List[Int]作烟,C1中String映射到C2中的List[String]。除此之外砾医,C1中的關(guān)系態(tài)射Int -> String也映射到C2中的關(guān)系List[Int] -> List[String]態(tài)射上拿撩。

換句話說,如果一個范疇內(nèi)部的所有元素可以映射為另一個范疇的元素如蚜,且元素間的關(guān)系也可以映射為另一個范疇元素間關(guān)系压恒,則認(rèn)為這兩個范疇之間存在映射影暴。所謂函子就是表示兩個范疇的映射。

澄清了函子的含義探赫,那么如何在程序中表達(dá)它型宙?

在Haskell中,函子是在其上可以map over的東西期吓。稍微有一點函數(shù)式編程經(jīng)驗早歇,一定會想到數(shù)組(Array)或者列表(List),確實如此讨勤。不過箭跳,在我們的例子中,List并不是一個具體的類型潭千,而是一個類型構(gòu)造子谱姓。舉個例子,構(gòu)造List[Int]刨晴,也就是把Int提升到List[Int]屉来,記作Int -> List[Int]。這表達(dá)了一個范疇的元素可以映射為另一個范疇的元素狈癞。

List具有map方法茄靠,不妨看看map的定義:

f :: A -> B
map :: f -> List[A] -> List[B]

具體到我們的例子當(dāng)中,就有:

f :: Int -> String
map :: f -> List[Int] -> List[String]

展開來看:

map :: Int -> String -> List[Int] -> List[String]

map的定義清晰地告訴我們:Int -> String這種關(guān)系可以映射為List[Int] -> List[String]這種關(guān)系蝶桶。這就表達(dá)了元素間的關(guān)系也可以映射為另一個范疇元素間關(guān)系慨绳。

所以類型構(gòu)造器List[T]就是一個函子。

理解了函子的概念真竖,接著繼續(xù)探究什么是自函子脐雪。我們已經(jīng)知道自函數(shù)就是把類型映射到自身類型,那么自函子就是把范疇映射到自身范疇恢共。

自函子是如何映射范疇的战秋,見下圖:

image.png

圖中表示的是一個將范疇映射到自身的自函子,而且還是一個特殊的Identity自函子讨韭。為什么這么說脂信?從函子的定義出發(fā),我們考察這個自函子透硝,始終有List[Int] -> List[Int]以及List[Int] -> List[String] -> List[Int] -> List[String]這兩種映射吉嚣。
我們表述成:

類型List[Int]映射到自己
態(tài)射f :: List[Int] -> List[String]映射到自己

我們記作:

F(List[Int]) = List[Int]
F(f) = f
其中,F(xiàn)是Functor.

除了Identity的自函子蹬铺,還有其它的自函子尝哆,見下圖:

[圖片上傳失敗...(image-db344c-1542218165324)]

圖中的省略號代表這些范疇可以無限地延伸下去。我們在這個大范疇所做的所有映射操作都是同一范疇內(nèi)的映射甜攀,自然這樣的范疇就是一個自函子的范疇秋泄。

我們記作:

List[Int] -> List[List[Int]]
List[Int] -> List[String] -> List[List[Int]] -> List[List[String]]
...

所以List[Int]琐馆、List[List[Int]]、...恒序、List[List[List[...]]]及其之間的態(tài)射是一個自函子的范疇瘦麸。


幺半群

[幺半群][1]是一個帶有二元運算 : M × M → M 的集合 M ,其符合下列公理:
結(jié)合律:對任何在 M 內(nèi)的a歧胁、b滋饲、c, (a
b)c = a(bc) 喊巍。
單位元:存在一在 M 內(nèi)的元素e屠缭,使得任一于 M 內(nèi)的 a 都會符合 a
e = e*a = a 。

接著我們看看在自函子的范疇上崭参,怎么結(jié)合幺半群的定義得出Monad的呵曹。

假設(shè)我們有個cube函數(shù),它的功能就是計算每個數(shù)的3次方何暮,函數(shù)簽名如下:

cube :: Number -> Number

現(xiàn)在我們想在其返回值上添加一些調(diào)試信息奄喂,所以返回一個元組(Tuple),第二個元素代表調(diào)試信息海洼。函數(shù)簽名如下:

f :: Number -> (Number,String)

入?yún)⒑统鰠⒉灰恢驴缧拢@會產(chǎn)生什么影響?我們看看幺半群的定義中規(guī)定的結(jié)合律坏逢。對于函數(shù)而言域帐,結(jié)合律就是將函數(shù)以各種結(jié)合方式嵌套起來調(diào)用。我們將常用的compose函數(shù)看作此處的二元運算词疼。

var compose = function(f, g) {
  return function(x) {
    return f(g(x));
  };
};

compose(f, f)

從函數(shù)簽名可以很容易看出,右邊的f運算的結(jié)果是元組帘腹,而左側(cè)的f卻是接收一個Number類型的函數(shù)贰盗,它們是彼此不兼容的。

有什么好辦法能消除這種不兼容性阳欲?結(jié)合前面所講舵盈,cube是一個自函數(shù)Number -> Number,而元組(Number,String)在Hask范疇是一個自函子球化,理由如下:

F Number = (Number,String) 
F Number -> Number = (Number,String) -> (Number,String)

假如輸入和輸出都是元組秽晚,結(jié)果會如何呢?

fn :: (Number,String) -> (Number,String)

compose(fn, fn)  

這樣是可行的筒愚!在驗證滿足結(jié)合律之前赴蝇,我們引入一個bind函數(shù)來輔助將f提升成fn.

f :: Number -> (Number,String) => fn :: (Number,String) -> (Number,String)
注: 在Haskell中稱為 liftM
var bind = function(f) {
  return function F(tuple) {
    var x  = tuple[0],
        s  = tuple[1],
        fx = f(x),
        y  = fx[0],
        t  = fx[1];

    return [y, s + t];
  };
};

我們來實現(xiàn)元組自函子范疇上的結(jié)合律:

var cube = function(x) {
  return [x * x * x, 'cube was called.'];
};

var sine = function(x) {
  return [Math.sin(x), 'sine was called.'];
};

var f = compose(compose(bind(sine), bind(cube)), bind(cube));
f([3, ''])

var f1 = compose(bind(sine), compose(bind(cube), bind(cube)));
f1([3,''])
>>>
[0.956, 'cube was called.cube was called.sine was called.']
[0.956, 'cube was called.cube was called.sine was called.']

這里f和f1代表的調(diào)用順序產(chǎn)生同樣的結(jié)果,說明元組自函子范疇滿足結(jié)合律巢掺。

那如何找到這樣一個e句伶,使得a*e=e*a=a劲蜻,此處的*compose運算

// unit :: Number -> (Number,String)
var unit = function(x) { return [x, ''] };

var f = compose(bind(sine), bind(cube));

compose(f, bind(unit)) = compose(bind(unit), f) = f

這里的bind(unit)就是e了。

到這里考余,思路逐步清晰了先嬉。在Haskell這類的強(qiáng)類型語言中,我們甚至可以組裝自己的Tuple Monad楚堤。如下:

type Tuple(Number,String)
flatmap :: Tuple -> Number -> Tuple -> Tuple
unit :: Number -> Tuple
map :: Tuple >>= unit
    :: Tuple -> Number -> Number -> Tuple

//compose
// flatmap 即 bind疫蔓,中綴表達(dá)式一般是 >>=
Tuple >>= (Number -> Tuple) >>= (Number -> Tuple)

Monads for functional programming一書中介紹說monad是一個三元組(M, unit, *),對應(yīng)此處就是(Tuple, unit, bind).

參考鏈接:

  1. Translation from Haskell to JavaScript of selected portions of the best introduction to monads I've ever read
  2. 我所理解的monad
  3. Monads for functional programming
  4. Functor, Applicative, Monad

函子functor是比函數(shù)更高階的函數(shù)身冬,函子是作用于兩個范疇之間的函數(shù)衅胀,但是根本上也是一個函數(shù),因此函子的類型與上面的函數(shù)類型差不多吏恭。假設(shè)兩個范疇是 C和D, 其函函子是:

functor F: C -> D

函子functor原理
  函數(shù)組合的方式有其特殊地方拗小,這個特殊主要是由于我們組合的對象是函數(shù),如果組合的對象是整數(shù)類型樱哼,兩個整數(shù)組合成一個整數(shù)哀九,這沒有問題,但是你不能將兩個函數(shù)類型組合起來還是和原來函數(shù)類型一樣搅幅。比如我們將兩個f函數(shù)f ∷ A → B組合起來阅束,就不會得到還是A → B。

函子functor是比函數(shù)更高階的函數(shù)茄唐,函子是作用于兩個范疇之間的函數(shù)息裸,可以簡單認(rèn)為是兩個集合之間的映射。范疇的映射轉(zhuǎn)換需要轉(zhuǎn)換其中的元素和態(tài)射沪编。

假設(shè)兩個范疇是 C和D, 有一個函子functor F: C -> D 呼盆,這種寫法類似函數(shù)寫法,但是因為函子是范疇的函數(shù)蚁廓,所以访圃,其工作原理是進(jìn)入范疇C和D內(nèi)部,而范疇是由元素對象和態(tài)射箭頭組成相嵌,因此函子就要分別作用于元素對象和態(tài)射箭頭腿时。

映射元素對象:C中的任何對象A轉(zhuǎn)變成了D中的F(A);
  映射態(tài)射箭頭:C中的態(tài)射f: A -> B轉(zhuǎn)變成了D中的F(f): F(A) -> F(B) 饭宾。
  (組合箭頭和元箭頭映射這里省略)

函子這種映射實際是一種分解組合方式批糟,對于這個過程我們可以用下面模擬形象地理解:

計算C集合中每個函數(shù)的"結(jié)果", 但是不組合它們.
將 F函數(shù)單獨應(yīng)用于C中每個函數(shù)的結(jié)果,我們就獲得結(jié)果的集合的集合看铆。
壓平這兩層集合徽鼎,組合所有的結(jié)果。 (注意這里的組合方式將對應(yīng)Monad的自然變換態(tài)射)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末纬傲,一起剝皮案震驚了整個濱河市满败,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌叹括,老刑警劉巖算墨,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異汁雷,居然都是意外死亡净嘀,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門侠讯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挖藏,“玉大人,你說我怎么就攤上這事厢漩∧っ撸” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵溜嗜,是天一觀的道長宵膨。 經(jīng)常有香客問我,道長炸宵,這世上最難降的妖魔是什么辟躏? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮土全,結(jié)果婚禮上捎琐,老公的妹妹穿的比我還像新娘。我一直安慰自己裹匙,他們只是感情好瑞凑,可當(dāng)我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著概页,像睡著了一般籽御。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上绰沥,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天篱蝇,我揣著相機(jī)與錄音贺待,去河邊找鬼徽曲。 笑死,一個胖子當(dāng)著我的面吹牛麸塞,可吹牛的內(nèi)容都是我干的秃臣。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼奥此!你這毒婦竟也來了弧哎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤稚虎,失蹤者是張志新(化名)和其女友劉穎撤嫩,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蠢终,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡序攘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了寻拂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片程奠。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖祭钉,靈堂內(nèi)的尸體忽然破棺而出瞄沙,到底是詐尸還是另有隱情,我是刑警寧澤慌核,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布距境,位于F島的核電站,受9級特大地震影響遂铡,放射性物質(zhì)發(fā)生泄漏肮疗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一扒接、第九天 我趴在偏房一處隱蔽的房頂上張望伪货。 院中可真熱鬧,春花似錦钾怔、人聲如沸碱呼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽愚臀。三九已至,卻和暖如春矾利,著一層夾襖步出監(jiān)牢的瞬間姑裂,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工男旗, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留舶斧,地道東北人。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓察皇,卻偏偏與公主長得像茴厉,于是被迫代替她去往敵國和親泽台。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容