譯者按: 近年來,函數式語言的特性都被其它語言學過去了。javascript
爲了保證可讀性,本文采用意譯而非直譯。另外,本文版權歸原做者全部,翻譯僅用於學習。java
若是你使用函數式編程,無論有沒有用過函數式語言,在某總程度上已經使用過Monad。可能大多數人都不知道什麼叫作Monad。在這篇文章中,我不會用數學公式來解釋什麼是Moand,也不使用Haskell,而是用JavaScript直接寫Monad。程序員
做爲一個函數式程序員,我首先來介紹一下基礎的複合函數:編程
const add1 = x => x + 1 const mul3 = x => x * 3 const composeF = (f, g) => { return x => f(g(x)) } const addOneThenMul3 = composeF(mul3, add1) console.log(addOneThenMul3(4)) // 打印 15 複製代碼
複合函數composeF
接收f
和g
兩個參數,而後返回值是一個函數。該函數接收一個參數x
, 先將函數g
做用到x
, 其返回值做爲另外一個函數f
的輸入。小程序
addOneThenMul3
是咱們經過composeF
定義的一個新的函數:由mul3
和add1
複合而成。微信小程序
接下來看另外一個實際的例子:咱們有兩個文件,第一個文件存儲了第二個文件的路徑,第二個文件包含了咱們想要取出來的內容。使用剛剛定義的複合函數composeF
, 咱們能夠簡單的搞定:數組
const readFileSync = path => { return fs.readFileSync(path.trim()).toString() } const readFileContentSync = composeF(readFileSync, readFileSync) console.log(readFileContentSync('./file1')) 複製代碼
readFileSync
是一個阻塞函數,接收一個參數path
,並返回文件中的內容。咱們使用composeF
函數將兩個readFileSync
複合起來,就達到咱們的目的。是否是很簡潔?微信
但若是readFile
函數是異步的呢?若是你用Node.js
寫過代碼的話,應該對回調很熟悉。在函數式語言裏面,有一個更加正式的名字:continuation-passing style 或則 CPS。markdown
咱們經過以下函數讀取文件內容:異步
const readFileCPS = (path, cb) => { fs.readFile( path.trim(), (err, data) => { const result = data.toString() cb(result) } ) } 複製代碼
可是有一個問題:咱們不能使用composeF
了。由於readCPS
函數自己不在返回任何東西。 咱們能夠從新定義一個複合函數composeCPS
,以下:
const composeCPS = (g, f) => { return (x, cb) => { g(x, y => { f(y, z => { cb(z) }) }) } } const readFileContentCPS = composeCPS(readFileCPS, readFileCPS) readFileContentCPS('./file1', result => console.log(result)) 複製代碼
注意:在composeCPS
中,我交換了參數的順序。composeCPS
會首先調用函數g
,在g
的回調函數中,再調用f
, 最終經過cb
返回值。
接下來,咱們來一步一步改進咱們定義的函數。
第一步,咱們稍微改寫一下readFIleCPS
函數:
const readFileHOF = path => cb => { readFileCPS(path, cb) } 複製代碼
HOF
是 High Order Function (高階函數)的縮寫。咱們能夠這樣理解readFileHOF
: 接收一個爲path
的參數,返回一個新的函數。該函數接收cb
做爲參數,並調用readFileCPS
函數。
而且,定義一個新的複合函數:
const composeHOF = (g, f) => { return x => cb => { g(x)(y => { f(y)(cb) }) } } const readFileContentHOF = composeHOF(readFileHOF, readFileHOF) readFileContentHOF('./file1')(result => console.log(result)) 複製代碼
第二步,咱們接着改進readFileHOF
函數:
const readFileEXEC = path => { return { exec: cb => { readFileCPS(path, cb) } } } 複製代碼
readFileEXEC
函數返回一個對象,對象中包含一個exec
屬性,並且exec
是一個函數。
一樣,咱們再改進複合函數:
const composeEXEC = (g, f) => { return x => { return { exec: cb => { g(x).exec(y => { f(y).exec(cb) }) } } } } const readFileContentEXEC = composeEXEC(readFileEXEC, readFileEXEC) readFileContentEXEC('./file1').exec(result => console.log(result)) 複製代碼
如今咱們來定義一個幫助函數:
const createExecObj = exec => ({exec}) 複製代碼
該函數返回一個對象,包含一個exec
屬性。 咱們使用該函數來優化readFileEXEC
函數:
const readFileEXEC2 = path => { return createExecObj(cb => { readFileCPS(path, cb) }) } 複製代碼
readFileEXEC2
接收一個path
參數,返回一個exec
對象。
接下來,咱們要作出重大改進,請注意! 迄今爲止,因此的複合函數的兩個參數都是huan'hnh函數,接下來咱們把第一個參數改爲exec
對象。
const bindExec = (execObj, f) => { return createExecObj(cb => { execObj.exec(y => { f(y).exec(cb) }) }) } 複製代碼
該bindExec
函數返回一個新的exec
對象。
咱們使用bindExec
來定義讀寫文件的函數:
const readFile2EXEC2 = bindExec( readFileEXEC2('./file1'), readFileEXEC2 ) readFile2EXEC2.exec(result => console.log(result)) 複製代碼
若是不是很清楚,咱們能夠這樣寫:
bindExec( readFileEXEC2('./file1'), readFileEXEC2 ) .exec(result => console.log(result)) 複製代碼
咱們接下來把bindExec
函數放入exec
對象中:
const createExecObj = exec => ({ exec, bind(f) { return createExecObj(cb => { this.exec(y => { f(y).exec(cb) }) }) } }) 複製代碼
如何使用呢?
readFileEXEC2('./file1') .bind(readFileEXEC2) .exec(result => console.log(result)) 複製代碼
這已經和在函數式語言Haskell裏面使用Monad幾乎如出一轍了。
咱們來作點重命名:
readFileAsync('./file1') .then(readFileAsync) .done(result => console.log(result)) 複製代碼
發現了嗎?居然是Promise!
從composeCPS
開始,都是Monad.
readFIleCPS
是Monad。事實上,它在Haskell裏面被稱做Cont Monad;exec 對象
是一個Monad。事實上,它在Haskell裏面被稱做IO Monad。bind
函數能夠把值從第一個參數Monad
中取出來,並調用第二個參數函數
。第二個函數要返回一個Monad。而且該返回的Monad類型要和第一個參數相同。Array.prototype.flatMap = function(f) { const r = [] for (var i = 0; i < this.length; i++) { f(this[i]).forEach(v => { r.push(v) }) } return r } const arr = [1, 2, 3] const addOneToThree = a => [a, a + 1, a + 2] console.log(arr.map(addOneToThree)) // [ [ 1, 2, 3 ], [ 2, 3, 4 ], [ 3, 4, 5 ] ] console.log(arr.flatMap(addOneToThree)) // [ 1, 2, 3, 2, 3, 4, 3, 4, 5 ] 複製代碼
咱們能夠驗證:
forEach
能夠獲取;flatMap
來做爲bind
函數。Monad是回調函數
? 根據性質3,是的。
回調函數式Monad
? 不是,除非有定義bind
函數。
Fundebug專一於JavaScript、微信小程序、微信小遊戲、支付寶小程序、React Native、Node.js和Java實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了6億+錯誤事件,獲得了Google、360、金山軟件等衆多知名用戶的承認。歡迎免費試用!
轉載時請註明做者Fundebug以及本文地址:
blog.fundebug.com/2017/06/21/…