譯者按: 近年來,函數式語言的特性都被其它語言學過去了。
原文:
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
接收
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。
我們通過如下函數讀取檔案内容:
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.
-
是Monad。事實上,它在Haskell裡面被稱作 Cont Monad ;readFIleCPS
-
是一個Monad。事實上,它在Haskell裡面被稱作 IO Monad 。exec 對象
Monad 有什麼性質呢?
- 它有一個環境;
- 這個環境裡面不一定有值;
- 提供一個擷取該值的方法;
- 有一個
函數可以把值從第一個參數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 ]
我們可以驗證:
- [] 是環境
- []可以為空,值不一定存在;
- 通過
可以擷取;forEach
- 我們定義了
來作為flatMap
bind
結論
-
Monad是回調函數
?
根據性質3,是的。
-
不是,除非有定義回調函數式Monad
bind
歡迎加入
我們Fundebug的全棧BUG監控交流群: 622902485。
版權聲明:
轉載時請注明作者Fundebug以及本文位址:
https://blog.fundebug.com/2017/06/21/write-monad-in-js/#more