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对象。
接下来,我们要做出重大改进,请注意!
迄今为止,所以的复合函数的两个参数都是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 有什么性质呢?
- 它有一个环境;
- 这个环境里面不一定有值;
- 提供一个获取该值的方法;
- 有一个
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
Promise是Monad吗?的更多相关文章
- 转评:你造promise就是monad吗
看到一遍好文章,与我的想法如出一辙,先转为敬.首先说说我对Monad和promise的理解: Monad的这种抽象方式是为了简化程序中不确定状态的判断而提出的,能够让程序员从更高的层次顺序描述程序逻辑 ...
- 函数式JS: 原来promise是这样的monad
转载请注明出处: http://hai.li/2017/03/27/prom... 背景 上篇文章 函数式JS: 一种continuation monad推导 得到了一个类似promise的链式调用, ...
- Js-函数式编程
前言 JavaScript是一门多范式语言,即可使用OOP(面向对象),也可以使用FP(函数式),由于笔者最近在学习React相关的技术栈,想进一步深入了解其思想,所以学习了一些FP相关的知识点,本文 ...
- Functional Programming without Lambda - Part 2 Lifting, Functor, Monad
Lifting Now, let's review map from another perspective. map :: (T -> R) -> [T] -> [R] accep ...
- 指令式Callback,函数式Promise:对node.js的一声叹息
原文:Callbacks are imperative, promises are functional: Node's biggest missed opportunity promises 天生就 ...
- Monad / Functor / Applicative 浅析
前言 Swift 其实比 Objective-C 复杂很多,相对于出生于上世纪 80 年代的 Objective-C 来说,Swift 融入了大量新特性.这也使得我们学习掌握这门语言变得相对来说更加困 ...
- 一个Monad的不严谨介绍
一个单子(Monad)说白了不过就是自函子范畴上的一个幺半群而已,这有什么难以理解的?* 之前了解了下Monad,后来一段时间没碰,最近研究Parser用到Monad时发现又不懂了.现在重新折腾,趁着 ...
- 【响应式编程的思维艺术】 (3)flatMap背后的代数理论Monad
目录 一. 划重点 二. flatMap功能解析 三. flatMap的推演 3.1 函数式编程基础知识回顾 3.2 从一个容器的例子开始 3.3 Monad登场 3.4 对比总结 3.5 一点疑问 ...
- 从函数式编程到Promise
译者按: 近年来,函数式语言的特性都被其它语言学过去了.JavaScript异步编程中大显神通的Promise,其实源自于函数式编程的Monad! 原文: Functional Computation ...
随机推荐
- post上传和压缩、插件模拟请求
gzip gzip一种压缩方式,或者是文件形式,它主要用于网络传输数据的压缩 gzip压缩好不好用 浏览器:网速一定.内容越小.请求响应的速度是不是更快 手机server:返回数据类型是json/ ...
- Kali学习笔记31:目录遍历漏洞、文件包含漏洞
文章的格式也许不是很好看,也没有什么合理的顺序 完全是想到什么写一些什么,但各个方面都涵盖到了 能耐下心看的朋友欢迎一起学习,大牛和杠精们请绕道 目录遍历漏洞: 应用程序如果有操作文件的功能,限制不严 ...
- gcc编译参数详解一(-ffunction-sections -fdata-sections)
背景 有时我们的程序会定义一些暂时使用不上的功能和函数,虽然我们不使用这些功能和函数,但它们往往会浪费我们的ROM和RAM的空间.这在使用静态库时,体现的更为严重.有时,我们只使用了静态库仅有的几个功 ...
- 数字音频处理的瑞士军刀sox的音效算法以及用法
SoX可以明确的写出需要的音频处理的效果,可以方便的重复使用,在目前的条件下是一个比较方便使用的项目.不过相信随着Audacity的发展,很有可能在未来可以逐渐替代SoX的功能. 对于SoX主要关心的 ...
- Robot Framework - 5 - 创建测试数据
Creating test data User Guide - Creating test data:http://robotframework.org/robotframework/latest/R ...
- axios 安卓低版本兼容性处理
问题: 在较低版本的android手机中发现封装的 http 无效,我测试使用的是android 4.4的老手机,主要就是无法使用promise. 解决方案 安装 npm install es6-pr ...
- rest-framework之权限组件
权限 权限 作用 : 校验用户是否有权限访问 检测权限肯定是在用户认证通过之后,所有可以直接在request中取出用户做判断 先定义一个类,继承 BasePermission. from rest_f ...
- Intellij idea常用快捷键和技巧
一.常用快捷键 搜索 double shift 全文搜索内容 ctrl + shift + f 搜索文件 Ctrl + shift + n 打开项目窗口 Alt + 1 智能代码补全 Ctrl+Sh ...
- [Jenkins][Git]ssh原理以及与https的区别
-------------------------------------------------------------------- 本文参考多篇文章结合自身情况完成,可自由转载,需保留本文出处! ...
- Java多线程之二(Synchronized)
常用API method 注释 run() run()方法是我们创建线程时必须要实现的方法,但是实际上该方法只是一个普通方法,直接调用并没有开启线程的作用. start() start()方法作用为使 ...