什么是Monad?

最近比較巧合的接觸了Functional programming,經(jīng)常接觸到Monad的概念(在Haskell和F#中),然而除了知道這個(gè)概念很難明白以外其他都不太清楚,粗略地看了幾篇相關(guān)文章講的很晦澀難懂。所以決定花一段時(shí)間將這個(gè)FP的奇怪概念搞個(gè)明白。

什么是monad?

wikipedia

太長(zhǎng)了,懶得看

恩我也是這么想的。。所以我一直沒(méi)看,我在這里嘗試著來(lái)用簡(jiǎn)單一些的文字說(shuō)明一下Monad的概念,有可能每段都有錯(cuò)誤,謹(jǐn)慎啊。

Monad是一個(gè)FP中的專有名詞,是一個(gè)含有變量的類,monad是Monad這個(gè)類的實(shí)例。這個(gè)類的作用是把一系列操作連接在一起。沒(méi)錯(cuò)你可能想到do關(guān)鍵字,比如最簡(jiǎn)單的IO實(shí)例:

do

putStrLn "What is your name?"

name <- getLine

putStrLn ("Welcome, " ++ name ++ "!")

在這里do之后的list里面我們做了三件事情,打印,讀取IO輸入,輸出結(jié)果。在這三句話中間傳遞了參數(shù)name,這是一個(gè)state,這個(gè)state存在的意義是使得我們能夠以pure functional的方式執(zhí)行這三句話。

然而do關(guān)鍵字是一個(gè)語(yǔ)法糖,在這個(gè)語(yǔ)法糖的背后是一個(gè)>>=,也就是bind操作符在支持這個(gè)操作的連續(xù)性。那么在這里說(shuō),Monad就是一個(gè)支持>>=操作符的類,就是一個(gè)能夠連接多個(gè)operation的類。沒(méi)錯(cuò)剛才那段代碼就是一個(gè)monad,就是一段由幾個(gè)function組成的control flow,bind操作符的作用就是讀取左邊操作的結(jié)果并且讓右邊操作能夠使用。很簡(jiǎn)單

這說(shuō)的太簡(jiǎn)單了,肯定是錯(cuò)的

是說(shuō)的很簡(jiǎn)單,但是不能說(shuō)是錯(cuò)的吧。。舉個(gè)例子,假如有個(gè)沒(méi)有概念的人問(wèn)你“什么是函數(shù)”,怎么回答?一段映射?讀取幾個(gè)參數(shù)輸出幾個(gè)參數(shù)的代碼?這么簡(jiǎn)單的概念你怎么使別人相信函數(shù)在編程過(guò)程中的重要作用呢,很難吧。Java中我們當(dāng)然可以寫一個(gè)函數(shù),讀取一個(gè)int返回一個(gè)int,這嚴(yán)格的遵守了數(shù)學(xué)函數(shù)的定義,但是同時(shí)這個(gè)函數(shù)還可以做很多其他的事情,比如打印出來(lái),比如進(jìn)一個(gè)死循環(huán),但是這些不是pure function能做的,haskell這樣的語(yǔ)言中function就應(yīng)該讀取一個(gè)值返回一個(gè)值,它對(duì)程序的影響只能體現(xiàn)在它的返回值上?!笆裁词呛瘮?shù)”這個(gè)問(wèn)題的答案大概長(zhǎng)什么樣子我想大家心里應(yīng)該有數(shù)。

Monad能發(fā)揮巨大作用,不是因?yàn)樗亩x太復(fù)雜,是因?yàn)樗恢皇呛?jiǎn)單的定義,而是可以延伸出無(wú)數(shù)個(gè)種類。沒(méi)錯(cuò)bind操作符的確就是簡(jiǎn)單地把參數(shù)從左邊傳給右邊,能包含bind操作符的都是monad,但是monad還可以同時(shí)做很多其他的事情,做的事情不一樣monad的作用也不一樣。換句話說(shuō),不同monad賦予了>>=不同的意義。

舉一個(gè)不是我想出來(lái)的例子,以下代碼是javascript的幾個(gè)函數(shù)。

var sine = function(x) { return Math.sin(x) };

var cube = function(x) { return x * x * x };

var sineCubed = cube(sine(x));

sineCubed是一個(gè)組合函數(shù),可以很簡(jiǎn)單的把它在Haskell中寫出來(lái)。然而這個(gè)時(shí)候我們隊(duì)sineCubed有個(gè)特殊的要求,要求它能夠打印出來(lái)自己運(yùn)行時(shí)的值。javascript中間在函數(shù)里加一句console.log即可,但是Haskell中間呢?沒(méi)辦法在sine:: Number -> Number這個(gè)函數(shù)中間加一句打印,那樣違反了pure function的原則。那么我們只能在返回值中體現(xiàn)出來(lái):

var sine = function(x) {

return [Math.sin(x), 'sine was called.'];

};

var cube = function(x) {

return [x * x * x, 'cube was called.'];

};

那么sineCubed此時(shí)應(yīng)該怎么寫?假設(shè)還是之前的寫法,那么我們會(huì)發(fā)現(xiàn)cube函數(shù)需要讀取一個(gè)數(shù)組了,無(wú)法執(zhí)行。這時(shí)候就需要多做一步處理,假設(shè)這個(gè)時(shí)候我在做一個(gè)作業(yè),只要完成作業(yè)即可,那我可能就是sineCubed中cube只讀取sine返回值的第一個(gè)參數(shù),或者改變cube的簽名為cube :: (Number,String) -> (Number,String)野蠻地完成任務(wù)(語(yǔ)言穿越了,只是為了表達(dá)意思)。這顯然不是best practice,更優(yōu)雅的方法是什么?沒(méi)錯(cuò)這位同學(xué)答對(duì)了,就是對(duì)參數(shù)進(jìn)行一個(gè)包裝和解包裝的工作,我們需要一個(gè)工具能夠做這個(gè)事情。

首先需要一個(gè)unit,它讀取一個(gè)Number,將這個(gè)number放在一個(gè)container里,返回一個(gè)(Number,String)。

// unit :: Number -> (Number,String)

var unit = function(x) { return [x, ''] };

unit函數(shù)使得原本簡(jiǎn)單的返回值可以被包裝成包含了其他信息的值。然后在lift函數(shù)中我們用到了unit:

// lift :: (Number -> Number) -> (Number -> (Number,String))

var lift = function(f) {

return function(x) {

return unit(f(x));

};

};

lift的簽名是讀取一個(gè)函數(shù),返回一個(gè)函數(shù),它將一個(gè)簡(jiǎn)單函數(shù)"lift"到了一個(gè)包含了一個(gè)其他信息的函數(shù)。要做sineCubed,我們還需要一個(gè)函數(shù)能夠組合幾個(gè)函數(shù),這是compose做的事情,它簡(jiǎn)單地把兩個(gè)函數(shù)復(fù)合起來(lái)??梢韵胂筮€需要一個(gè)bind,它使得原本的sine和cube的函數(shù)簽名能夠被修改成想要的類型。這幾個(gè)抽象的函數(shù)概念就組成了一個(gè)monad,實(shí)際上bind和unit就組成了一個(gè)monad,剛剛做的事情其實(shí)就是Haskell的Writer monad所做的事情。打開(kāi)這個(gè)鏈接看看,應(yīng)該你就能懂了。

這么說(shuō),monad其實(shí)是一種design pattern?

我個(gè)人覺(jué)得拿javascript去形容Haskell中的概念是一件容易誤導(dǎo)人的事情,之前在一篇很長(zhǎng)的blog中也看到說(shuō)“不要用其他語(yǔ)言的思維去考慮函數(shù)式語(yǔ)言”。說(shuō)monad是一種design pattern是有一定道理的,假設(shè)你在程序中需要一個(gè)函數(shù)接受一類輸入,得到另外一類的輸出,那么就要考慮用到bind和unit這樣的函數(shù),unit函數(shù)包裝參數(shù)的類得到需要的另外一種類型,bind函數(shù)修改原函數(shù)使得它能夠接受自己返回的函數(shù)類型。這樣做的好處是可以在達(dá)到目的的同時(shí),避免對(duì)原來(lái)的代碼做出“毀滅性”的徹底修改。

然而之上說(shuō)的只是一種monad類型,并不適用到全部范圍。我們來(lái)看看其它幾種monad:

  • 在一系列操作中,每一步都返回一個(gè)success/failure的標(biāo)志,只有success才執(zhí)行下一步,failure則自動(dòng)終止。這是Failure Monad。

  • 將返回標(biāo)志改成Exception的處理,這是Error Monad, Exception Monad,如何處理完全可以自定義。

  • 每一步返回多個(gè)結(jié)果,在下一步遍歷這些結(jié)果,進(jìn)行篩選或者處理,這是List Monad。

  • 每一步操作都是針對(duì)state的一個(gè)action,下一步操作只從上一步操作返回的world status得到信息進(jìn)行操作,bind操作使得IO的side effects能夠保證按順序處理,這是I/O Monad。(這里說(shuō)的太籠統(tǒng),最好再看看I/O Monad的說(shuō)明。。)
    更合適的說(shuō),Monad是一個(gè)通用的將各個(gè)函數(shù)作為組建搭建起來(lái)的“積木”,這個(gè)積木有兩個(gè)基本部件"return"和">>=",而且這兩個(gè)部件滿足一些特定的組合性質(zhì),那么我就可以說(shuō)我搭建的是一個(gè)monad:

  1. (return x) >>= f == f x
  2. m >>= return == m
  3. (m >>= f) >>= g == m >>= (\x -> f x >>= g)

orz...

這些事情,不用Monad當(dāng)然也能做到,但是Monad的意義是使得達(dá)到這些目的的方法簡(jiǎn)單很多,只需要去定義>>=做什么事情即可達(dá)到目的。

打字好累。。stackoverflow的這個(gè)鏈接有很多大牛講解了自己對(duì)Monad的理解,看完這篇日志之后再去看這個(gè)鏈接可能會(huì)稍微多理解一些東西(或者可以發(fā)現(xiàn)我說(shuō)錯(cuò)了那麻煩告訴我一下=,=)

第一次在簡(jiǎn)書上寫日志,markdown的設(shè)置弄了很久。。希望這篇日志能夠?qū)Υ蠹矣悬c(diǎn)小幫助

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容