译者按: 近年来,函数式语言的特性都被其它语言学过去了。JavaScript异步编程中大显神通的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
接收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
对象。
接下来,咱们要作出重大改进,请注意!
迄今为止,全部的复合函数的两个参数都是函数,接下来咱们把第一个参数改为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
Fundebug专一于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了7亿+错误事件,获得了Google、360、金山软件、百姓网等众多知名用户的承认。欢迎免费试用!
转载时请注明做者Fundebug以及本文地址: