最近比較巧合的接觸了Functional programming坐昙,經常接觸到Monad的概念(在Haskell和F#中),然而除了知道這個概念很難明白以外其他都不太清楚芋忿,粗略地看了幾篇相關文章講的很晦澀難懂炸客。所以決定花一段時間將這個FP的奇怪概念搞個明白。
什么是monad?
太長了盗飒,懶得看
恩我也是這么想的嚷量。。所以我一直沒看逆趣,我在這里嘗試著來用簡單一些的文字說明一下Monad的概念,有可能每段都有錯誤嗜历,謹慎啊宣渗。
Monad是一個FP中的專有名詞,是一個含有變量的類梨州,monad是Monad這個類的實例痕囱。這個類的作用是把一系列操作連接在一起。沒錯你可能想到do關鍵字暴匠,比如最簡單的IO實例:
do
putStrLn "What is your name?"
name <- getLine
putStrLn ("Welcome, " ++ name ++ "!")
在這里do之后的list里面我們做了三件事情鞍恢,打印,讀取IO輸入,輸出結果帮掉。在這三句話中間傳遞了參數name弦悉,這是一個state,這個state存在的意義是使得我們能夠以pure functional的方式執(zhí)行這三句話蟆炊。
然而do關鍵字是一個語法糖稽莉,在這個語法糖的背后是一個>>=
,也就是bind操作符在支持這個操作的連續(xù)性涩搓。那么在這里說污秆,Monad就是一個支持>>=
操作符的類,就是一個能夠連接多個operation的類昧甘。沒錯剛才那段代碼就是一個monad良拼,就是一段由幾個function組成的control flow,bind操作符的作用就是讀取左邊操作的結果并且讓右邊操作能夠使用充边。很簡單
這說的太簡單了将饺,肯定是錯的
是說的很簡單,但是不能說是錯的吧痛黎。予弧。舉個例子,假如有個沒有概念的人問你“什么是函數”湖饱,怎么回答掖蛤?一段映射?讀取幾個參數輸出幾個參數的代碼井厌?這么簡單的概念你怎么使別人相信函數在編程過程中的重要作用呢蚓庭,很難吧。Java中我們當然可以寫一個函數仅仆,讀取一個int返回一個int器赞,這嚴格的遵守了數學函數的定義,但是同時這個函數還可以做很多其他的事情墓拜,比如打印出來港柜,比如進一個死循環(huán),但是這些不是pure function能做的咳榜,haskell這樣的語言中function就應該讀取一個值返回一個值夏醉,它對程序的影響只能體現(xiàn)在它的返回值上∮亢“什么是函數”這個問題的答案大概長什么樣子我想大家心里應該有數畔柔。
Monad能發(fā)揮巨大作用,不是因為它的定義太復雜臣樱,是因為他不只是簡單的定義靶擦,而是可以延伸出無數個種類腮考。沒錯bind操作符的確就是簡單地把參數從左邊傳給右邊,能包含bind操作符的都是monad玄捕,但是monad還可以同時做很多其他的事情踩蔚,做的事情不一樣monad的作用也不一樣。換句話說桩盲,不同monad賦予了>>=
不同的意義寂纪。
舉一個不是我想出來的例子,以下代碼是javascript的幾個函數赌结。
var sine = function(x) { return Math.sin(x) };
var cube = function(x) { return x * x * x };
var sineCubed = cube(sine(x));
sineCubed是一個組合函數捞蛋,可以很簡單的把它在Haskell中寫出來。然而這個時候我們隊sineCubed有個特殊的要求柬姚,要求它能夠打印出來自己運行時的值拟杉。javascript中間在函數里加一句console.log即可,但是Haskell中間呢量承?沒辦法在sine:: Number -> Number
這個函數中間加一句打印搬设,那樣違反了pure function的原則。那么我們只能在返回值中體現(xiàn)出來:
var sine = function(x) {
return [Math.sin(x), 'sine was called.'];
};
var cube = function(x) {
return [x * x * x, 'cube was called.'];
};
那么sineCubed此時應該怎么寫撕捍?假設還是之前的寫法拿穴,那么我們會發(fā)現(xiàn)cube函數需要讀取一個數組了,無法執(zhí)行忧风。這時候就需要多做一步處理默色,假設這個時候我在做一個作業(yè),只要完成作業(yè)即可狮腿,那我可能就是sineCubed中cube只讀取sine返回值的第一個參數腿宰,或者改變cube的簽名為cube :: (Number,String) -> (Number,String)
野蠻地完成任務(語言穿越了,只是為了表達意思)缘厢。這顯然不是best practice吃度,更優(yōu)雅的方法是什么?沒錯這位同學答對了贴硫,就是對參數進行一個包裝和解包裝的工作椿每,我們需要一個工具能夠做這個事情。
首先需要一個unit夜畴,它讀取一個Number拖刃,將這個number放在一個container里,返回一個(Number,String)贪绘。
// unit :: Number -> (Number,String)
var unit = function(x) { return [x, ''] };
unit函數使得原本簡單的返回值可以被包裝成包含了其他信息的值。然后在lift函數中我們用到了unit:
// lift :: (Number -> Number) -> (Number -> (Number,String))
var lift = function(f) {
return function(x) {
return unit(f(x));
};
};
lift的簽名是讀取一個函數央碟,返回一個函數税灌,它將一個簡單函數"lift"到了一個包含了一個其他信息的函數均函。要做sineCubed,我們還需要一個函數能夠組合幾個函數菱涤,這是compose做的事情苞也,它簡單地把兩個函數復合起來≌掣眩可以想象還需要一個bind如迟,它使得原本的sine和cube的函數簽名能夠被修改成想要的類型。這幾個抽象的函數概念就組成了一個monad攻走,實際上bind和unit就組成了一個monad殷勘,剛剛做的事情其實就是Haskell的Writer
monad所做的事情。打開這個鏈接看看昔搂,應該你就能懂了玲销。
這么說,monad其實是一種design pattern?
我個人覺得拿javascript去形容Haskell中的概念是一件容易誤導人的事情摘符,之前在一篇很長的blog中也看到說“不要用其他語言的思維去考慮函數式語言”贤斜。說monad是一種design pattern是有一定道理的,假設你在程序中需要一個函數接受一類輸入逛裤,得到另外一類的輸出瘩绒,那么就要考慮用到bind和unit這樣的函數,unit函數包裝參數的類得到需要的另外一種類型带族,bind函數修改原函數使得它能夠接受自己返回的函數類型锁荔。這樣做的好處是可以在達到目的的同時,避免對原來的代碼做出“毀滅性”的徹底修改炉菲。
然而之上說的只是一種monad類型堕战,并不適用到全部范圍。我們來看看其它幾種monad:
在一系列操作中拍霜,每一步都返回一個success/failure的標志嘱丢,只有success才執(zhí)行下一步,failure則自動終止祠饺。這是Failure Monad越驻。
將返回標志改成Exception的處理,這是Error Monad道偷, Exception Monad缀旁,如何處理完全可以自定義。
每一步返回多個結果勺鸦,在下一步遍歷這些結果并巍,進行篩選或者處理,這是List Monad换途。
每一步操作都是針對state的一個action懊渡,下一步操作只從上一步操作返回的world status得到信息進行操作刽射,bind操作使得IO的side effects能夠保證按順序處理,這是I/O Monad剃执。(這里說的太籠統(tǒng)誓禁,最好再看看I/O Monad的說明。肾档。)
更合適的說摹恰,Monad是一個通用的將各個函數作為組建搭建起來的“積木”,這個積木有兩個基本部件"return"和">>="怒见,而且這兩個部件滿足一些特定的組合性質俗慈,那么我就可以說我搭建的是一個monad:
- (return x) >>= f == f x
- m >>= return == m
- (m >>= f) >>= g == m >>= (\x -> f x >>= g)
orz...
這些事情,不用Monad當然也能做到速种,但是Monad的意義是使得達到這些目的的方法簡單很多姜盈,只需要去定義>>=
做什么事情即可達到目的。
打字好累配阵。馏颂。stackoverflow的這個鏈接有很多大牛講解了自己對Monad的理解,看完這篇日志之后再去看這個鏈接可能會稍微多理解一些東西(或者可以發(fā)現(xiàn)我說錯了那麻煩告訴我一下=棋傍,=)
第一次在簡書上寫日志救拉,markdown的設置弄了很久。瘫拣。希望這篇日志能夠對大家有點小幫助