Promise是Monad嗎?


譯者按: 近年來,函數式語言的特性都被其它語言學過去了。

 

原文: Functional Computational Thinking — What is a monad?

譯者: Fundebug

為了保證可讀性,本文采用意譯而非直譯。另外,本文版權歸原作者所有,翻譯僅用於學習。

如果你使用函數式編程,不管有沒有用過函數式語言,在某總程度上已經使用過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接收fg兩個參數,然后返回值是一個函數。該函數接收一個參數x, 先將函數g作用到x, 其返回值作為另一個函數f的輸入。

addOneThenMul3是我們通過composeF定義的一個新的函數:由mul3add1復合而成。

接下來看另一個實際的例子:我們有兩個文件,第一個文件存儲了第二個文件的路徑,第二個文件包含了我們想要取出來的內容。使用剛剛定義的復合函數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。

我們通過如下函數讀取文件內容:

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幾乎一模一樣了。

我們來做點重命名:

  • readFileEXEC2 -> readFileAsync
  • bind -> then
  • exec -> done
readFileAsync( './file1')
.then(readFileAsync)
.done( result => console.log(result))

發現了嗎?竟然是Promise!

Monad在哪里呢?

composeCPS開始,都是Monad.

  • readFIleCPS是Monad。事實上,它在Haskell里面被稱作Cont Monad
  • exec 對象是一個Monad。事實上,它在Haskell里面被稱作IO Monad

Monad 有什么性質呢?

  1. 它有一個環境;
  2. 這個環境里面不一定有值;
  3. 提供一個獲取該值的方法;
  4. 有一個bind函數可以把值從第一個參數Monad中取出來,並調用第二個參數函數。第二個函數要返回一個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 ]

我們可以驗證:

  1. [] 是環境
  2. []可以為空,值不一定存在;
  3. 通過forEach可以獲取;
  4. 我們定義了flatMap來作為bind函數。

結論

  • Monad是回調函數 ?
    根據性質3,是的。

  • 回調函數式Monad?
    不是,除非有定義bind函數。

歡迎加入我們Fundebug全棧BUG監控交流群: 622902485


 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM