函数式JS: 原来promise是这样的monad
转载请注明出处: http://hai.li/2017/03/27/prom...
背景
上篇文章 函数式JS: 一种continuation monad推导 得到了一个类似promise的链式调用,引发了这样的思考:难道promise是monad?如果是的话又是怎样的monad呢?来来来,哥哥带你推倒,哦,不,是推导一下!
Monad
Monad是haskell里很重要的概念,作为一种类型,有着固定的操作方法,简单的可以类比面向对象的接口。
定义
unit :: a -> Monad a
flatMap :: Monad a -> (a -> Monad b) -> Monad b
这是类型签名的表述。unit
的作用可以理解为将a
放入容器中变成Monad a
。而当flatMap
转为(a -> Monad b) -> (Monad a -> Monad b)
时,它的作用就可以理解为将a -> Monad b
函数转换成Monad a -> Monad b
函数。
法则
flatMap(unit(x), f) ==== f(x) //左单位元
flatMap(monad, unit) ==== monad //右单位元
flatMap(flatMap(monad, f), g) ==== flatMap(monad, function(x) { flatMap(f(x), g) }) //关联性
这里x
是一般的值,f
和g
是一般的函数,monad
是一个Monad
类型的值,可以这么理解:
左单位元法则就是将包裹
unit(x)
和函数f
传给flatMap
执行等价于将包裹中的值x
抽出传给函数f
执行右单位元法则就是将包裹
monad
和函数unit
传给flatMap
执行等价于包裹monad
本身(有点像1*1=1
)关联性法则就是将包裹
monad
和函数f
传给flatMap
执行,再将执行的结果和函数g
传给flatMap
执行等价于将包裹monad
中的值x
抽出传给f
执行(执行结果依然是Monad
类型),再将执行结果中的值x
抽出传给g
执行
Promise
链式调用
new Promise(function(resolve) { setTimeout(function() { resolve("0") }, 100) })
.then(function(v) {
console.log(v);
return new Promise(function(resolve) { resolve(v + "->1") })
})
.then(function(v) {
console.log(v);
return new Promise(function(resolve) { setTimeout(function() { resolve(v + "->2") }, 1000) })
})
.then(console.log)
分析
先将Promise
链式调用整理一下,将关注点集中在链式调用上
function f0(resolve) { setTimeout(function() { resolve("0") }, 100) }
function f1(v) { console.log(v); return new Promise(function(resolve) { resolve(v + "->1") }) }
function f2(v) { console.log(v); return new Promise(function(resolve) { setTimeout(function() { resolve(v + "->2") }, 1000) }) }
function f3(v) { console.log(v) }
new Promise(f0).then(f1).then(f2).then(f3) //0 0->1 0->1->2
从unit
和flatMap
的特性可以直观地对应为
function g0(resolve) { setTimeout(function() { resolve("0") }, 100) }
function g1(v) { console.log(v); return unit(function(resolve) { resolve(v + "->1") }) }
function g2(v) { console.log(v); return unit(function(resolve) { setTimeout(function() { resolve(v + "->2") }, 1000) }) }
function g3(v) { console.log(v) }
unit(g0).flatMap(g1).flatMap(g2).flatMap(f3)
而对象的方法可以通过将this
作为参数传入方便地转为直接的函数,比如
var a = {method: function f(v){ console.log(this, v) }}
var a_method = function(t, v){ console.log(t, v) }
a.method("a") === a_method(a, "a")
这样将链式调用转为嵌套函数调用变成
function g0(resolve) { setTimeout(function() { resolve("0") }, 100) }
function g1(v) { console.log(v); return unit(function(resolve) { resolve(v + "->1") }) }
function g2(v) { console.log(v); return unit(function(resolve) { setTimeout(function() { resolve(v + "->2") }, 1000) }) }
function g3(v) { console.log(v) }
flatMap(
flatMap(
flatMap(
unit(g0)
)(g1)
)(g2)
)(g3)
这样如果unit
和flatMap
这两个直接函数可以构造推导出来,就可以窥探Promise
的真面目了。同学们!这道题!必考题!头两年不考,今年肯定考!
构造推导unit
function unit(f){ return f}
由
flatMap :: Monad a -> (a -> Monad b) -> Monad b
和flatMap(unit(g0))(g1)
可知传入g1
的参数就是a
,对应着"0"
。但由
unit :: a -> Monad a
和unit(g0)
得到的a
却对应着g0
。实际上a
对应着"0"
,只是a
在g0
里作为立即量传入,在g1
和g2
的返回值中作为闭包引用传入。Monad
可看作容器,那用什么做的容器呢?既然作为参数传入unit
的函数f
已经包裹了a
,那试试直接作为Monad a
返回。同时根据g0
看出返回值f
是用回调返回值的。也就是将一个用回调返回结果的函数作为容器。
构造推导flatMap
function flatMap(ma){
return function(g) {
var b=[], ga, h=[];
ma(function(a) { //1. 解包`ma`取出`a`
ga = g(a); //2. 将`a`传到`g`中执行
(ga && ga.flatMap ? ga : unit(function(c) { c(ga) })) //处理g没返回unit情况
(function(v) {
b.push(v); // 1.1—1.2—1.3
h.map(function(c) {c(v)}) //1.1—1.3—1.2
})
});
return unit(function(c) { //3. 将执行结果`b`包裹成`mb`返回
b.length
? b.map(c) // 1.1—1.2—1.3—2.1
: h.push(c) //1.1—1.3—1.2—2.1
})
}
}
由
flatMap :: Monad a -> (a -> Monad b) -> Monad b
知道flatMap
传入Monad a
返回函数,这个函数接收(a -> Monad b)
返回Monad b
,而(a -> Monad b)
对应g1
。可以构造flatMap
如下function flatMap(ma){return function(g1) { /*b = g1(a);*/ return mb }}
实际
flatMap
做了3步工作解包
ma
取出a
将
a
传到g1
中执行将执行结果
b
包裹成mb
返回
这里
ma
和g1
都是容器,通过回调得到输出结果,所以在ma
的回调中执行g1(a)
,再在g1(a)
的回调中得到执行结果v
,再将执行结果v
赋值给外部变量b
,最后将b
用unit
包裹成Monad b
返回。function flatMap(ma){
return function(g1) {
var b;
ma(function(a) {
g1(a)(function(v) {
b = v
})
});
return unit(function(c) {c(b)})
}
}如果
g1
是立即执行的话,第flatMap
的执行步骤是1--2--3,但如果2延迟执行步骤就变成了1--3--2,算上下一个flatMap
就是1.1--1.3--1.2--2.1。2.1的ma
就是1.2的mb
,2.1的ma
的参数c
中执行了2.2和2.3,也就是1.3的c
决定着2.1之后的步骤。如果将c
赋值给b
就可以在1.2执行完后才继续2.1之后的步骤,也就是:+--------------+
1.1—1.2—1.3—2.1 2.2—2.3function flatMap(ma){
return function(g1) {
var b;
ma(function(a) {
g1(a)(function(v) {
b(v)
})
});
return unit(function(c) { b = c })
}
}为了
flatMap
可以链接多个flatMap
,也就是一个1.3被多个2.1消化,需要保存所有在2.1后的执行链c
,用数组h
解决。function flatMap(ma){
return function(g1) {
var h=[];
ma(function(a) {
g1(a)(function(v) {
h.map(function(c) {c(v)})
})
});
return unit(function(c) { h.push(c) })
}
}整合1.2立即执行和延迟执行情况,同时适配多个1.3被多个2.1消化的情况,代码如下:
function flatMap(ma){
return function(g1) {
var b=[], h=[];
ma(function(a) {
g1(a)(function(v) {
b.push(v); // 1.1—1.2—1.3
h.map(function(c) {c(v)}) //1.1—1.3—1.2
})
});
return unit(function(c) {
b.length
? b.map(c) // 1.1—1.2—1.3—2.1
: h.push(c) //1.1—1.3—1.2—2.1
})
}
}由于
g3
没有返回mb
,所以还要加上对g1
返回的不是容器的处理,代码如下:function flatMap(ma){
return function(g1) {
var b=[], g1a, h=[];
ma(function(a) {
g1a = g1(a);
(g1a && typeof(g1a) == "function" ? g1a : unit(function(c) { c(g1a) }))
(function(v) {
b.push(v); // 1.1—1.2—1.3
h.map(function(c) {c(v)}) //1.1—1.3—1.2
})
});
return unit(function(c) {
b.length
? b.map(c) // 1.1—1.2—1.3—2.1
: h.push(c) //1.1—1.3—1.2—2.1
})
}
}现在可以测试下代码了
function unit(f){ return f }
function flatMap(ma) {
return function(g) {
var b=[], ga, h=[];
ma(function(a) { //1. 解包`ma`取出`a`
ga = g(a); //2. 将`a`传到`g`中执行
(ga && typeof(ga) == "function"? ga : unit(function(c) { c(ga) })) //处理g没返回unit情况
(function(v) {
b.push(v); // 1.1—1.2—1.3
h.map(function(c) {c(v)}) //1.1—1.3—1.2
})
});
return unit(function(c) { //3. 将执行结果`b`包裹成`mb`返回
b.length
? b.map(c) // 1.1—1.2—1.3—2.1
: h.push(c) //1.1—1.3—1.2—2.1
})
}
}
function g0(resolve) { setTimeout(function() { resolve("0") }, 100) }
function g1(v) { console.log(v); return unit(function(resolve) { resolve(v + "->1") }) }
function g2(v) { console.log(v); return unit(function(resolve) { setTimeout(function() { resolve(v + "->2") }, 1000) }) }
function g3(v) { console.log(v) }
flatMap(
flatMap(
flatMap(
unit(g0)
)(g1)
)(g2)
)(g3)
整合代码
现在将嵌套函数变回链式调用,这里也可以用是否有flatMap
方法来判断g1
是否返回容器
function unit(ma) {
ma.flatMap = function(g){
var b=[], ga, h=[];
ma(function(a) { //1. 解包`ma`取出`a`
ga = g(a); //2. 将`a`传到`g`中执行
(ga && ga.flatMap ? ga : unit(function(c) { c(ga) })) //处理g没返回unit情况
(function(v) {
b.push(v); // 1.1—1.2—1.3
h.map(function(c) {c(v)}) //1.1—1.3—1.2
})
});
return unit(function(c) { //3. 将执行结果`b`包裹成`mb`返回
b.length
? b.map(c) // 1.1—1.2—1.3—2.1
: h.push(c) //1.1—1.3—1.2—2.1
})
}
return ma
}
function g0(resolve) { setTimeout(function() { resolve("0") }, 100) }
function g1(v) { console.log(v); return unit(function(resolve) { resolve(v + "->1") }) }
function g2(v) { console.log(v); return unit(function(resolve) { setTimeout(function() { resolve(v + "->2") }, 1000) }) }
function g3(v) { console.log(v) }
unit(g0).flatMap(g1).flatMap(g2).flatMap(g3)
Promise是Monad吗?
将整合代码中unit
改成newPromise
,flatMap
改成then
,哇塞,除了new Promise
中的空格请问哪里有差?虽然改成构造函数使得newPromise
改成new Promise
也是分分钟的事情,但重点不是这个,重点是Promise是Monad吗?粗看是!
function newPromise(ma) {
ma.then = function(g){
var b=[], ga, h=[];
ma(function(a) {
ga = g(a);
(ga && ga.then ? ga : newPromise(function(c) { c(ga) }))
(function(v) { b.push(v); h.map(function(c) {c(v)}) })
});
return newPromise(function(c) { b.length ? b.map(c) : h.push(c) })
}
return ma
}
newPromise(function(resolve) { setTimeout(function() { resolve("0") }, 100) })
.then(function(v) {
console.log(v);
return newPromise(function(resolve) { resolve(v + "->1") })
})
.then(function(v) {
console.log(v);
return newPromise(function(resolve) { setTimeout(function() { resolve(v + "->2") }, 1000) })
})
.then(console.log)
符合定义
unit :: a -> Monad a
flatMap :: Monad a -> (a -> Monad b) -> Monad b
将定义改下名
newPromise :: a -> Monad a
then :: Monad a -> (a -> Monad b) -> Monad b
newPromise
的输入是一个函数,但在推导构造unit
里解释过,这里借助了立即量和闭包引用来额外增加了输入a
,newPromise
的参数则作为构造unit
的补充逻辑。newPromise
的输出是a
的包裹Monad a
。newPromise
的方法then
借助了闭包引用额外输入了Monad a
,而输入的g
函数输入是a
输出则是借助newPromise
实现的Monad b
。newPromise
的方法then
输出的是借助newPromise
实现的Monad b
,这里和g
的输出Monad b
不是同一个Monad
但是b
确实相同的。
符合法则
flatMap(unit(x), f) === f(x) //左单位元
flatMap(monad, unit) === monad //右单位元
flatMap(flatMap(monad, f), g) === flatMap(monad, function(x) { flatMap(f(x), g) }) //关联性
将法则改下名,同时改为链式调用
newPromise(x).then(f) ==== f(x) //左单位元
monad.then(newPromise) ==== monad //右单位元
monad.then(f).then(g) ==== monad.then(function(x) { f(x).then(g) }) //关联性
左单位元法则验证代码如下:
function newPromise(ma) {
ma.then = function(g){
var b=[], ga, h=[];
ma(function(a) {
ga = g(a);
(ga && ga.then ? ga : newPromise(function(c) { c(ga) }))
(function(v) { b.push(v); h.map(function(c) {c(v)}) })
});
return newPromise(function(c) { b.length ? b.map(c) : h.push(c) })
}
return ma
}
var x = 1;
var f = function(v){ return v + 2 }
newPromise(function(resolve) { resolve(x) }).then(f).then(console.log) //3
console.log(f(x)) //3右单位元法则验证代码如下:
function newPromise(ma) {
ma.then = function(g){
var b=[], ga, h=[];
ma(function(a) {
ga = g(a);
(ga && ga.then ? ga : newPromise(function(c) { c(ga) }))
(function(v) { b.push(v); h.map(function(c) {c(v)}) })
});
return newPromise(function(c) { b.length ? b.map(c) : h.push(c) })
}
return ma
}
newPromise(function(resolve) { resolve(1) }).then(newPromise).then(console.log) //1
newPromise(function(resolve) { resolve(1) }).then(console.log) //1关联性法则验证代码如下:
function newPromise(ma) {
ma.then = function(g){
var b=[], ga, h=[];
ma(function(a) {
ga = g(a);
(ga && ga.then ? ga : newPromise(function(c) { c(ga) }))
(function(v) { b.push(v); h.map(function(c) {c(v)}) })
});
return newPromise(function(c) { b.length ? b.map(c) : h.push(c) })
}
return ma
}
var f = function(v) { return newPromise(function(resolve) { resolve(v+2) }) }
var g = function(v) { return newPromise(function(resolve) { resolve(v+3) }) }
newPromise(function(resolve) { resolve(1) }).then(f).then(g).then(console.log) //6
newPromise(function(resolve) { resolve(1) }).then(function(x) { return f(x).then(g) }).then(console.log) //6
如此,原来Promise是这样的Monad!
参考
函数式JS: 原来promise是这样的monad的更多相关文章
- 函数式 js 接口实现原理,以及 lodash/fp 模块
函数式 js 接口 之前在 youtube 上看到一个技术视频,讲“underscore.js的接口为什么不好用”,以及什么样的接口更好用.演讲者是 lodash.js 的作者,他提出了一种“全面函数 ...
- 关于学习js的Promise的心得体会
最近一直在研究js的Promise对象,其中有一篇blog写得比较通俗易懂,转发如下: http://www.cnblogs.com/lvdabao/p/es6-promise-1.html 参照上面 ...
- 从函数式编程到Promise
译者按: 近年来,函数式语言的特性都被其它语言学过去了.JavaScript异步编程中大显神通的Promise,其实源自于函数式编程的Monad! 原文: Functional Computation ...
- js的Promise学习笔记(1)
1: 何为Promise Promise是抽象异步处理对象以及对其对象进行各种操作的组件,是基于并列/并行处理设计的一种编程语言. 说到基于JavaScript的异步处理,大多数都会想到利用回调函数. ...
- 讲解JS的promise,这篇是专业认真的!
http://www.zhangxinxu.com/wordpress/2014/02/es6-javascript-promise-%E6%84%9F%E6%80%A7%E8%AE%A4%E7%9F ...
- Angular JS中 Promise用法
一.Promise形象讲解A promise不是angular首创的,作为一种编程模式,它出现在1976年,比js还要古老得多.promise全称是 Futures and promises. 而在j ...
- Node.js之Promise维护(同步)多个回调(异步)状态
金天:学习一个新东西,就要持有拥抱的心态,如果固守在自己先前的概念体系,就会有举步维艰的感觉..NET程序员初用node.js最需要适应的就是异步开发, 全是异步,常规逻辑下遍历列表都是异步,如何保证 ...
- Node.js之Promise
2015年发布了ES6标准,所谓 Promise,就是ES6标准的一个对象,用来传递异步操作的消息.它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可供进一步 ...
- JS 中Promise 模式
异步模式在web编程中变得越来越重要,对于web主流语言Javscript来说,这种模式实现起来不是很利索,为此,许多Javascript库(比如 jQuery和Dojo)添加了一种称为promise ...
随机推荐
- angular4过滤器
Angular4中过滤器 一.大小写转换过滤器 uppercase将字符串转换为大写 lowercase将字符串转换为小写 <p>将字符串转换为大写{{str | uppercase}}& ...
- 读书笔记-构建高性能Web站点
基本概念 带宽:通常说的带宽比如8M带宽,是指主机与互联网运营商的交换机之间的数据传输速度,因为数据链路层的流量是通过控制接收方实现的.而百兆网卡则是指网卡的发送速度为100Mbit/s,则是指网卡发 ...
- (转载) Android studio如何生成aar包
Android studio如何生成aar包 标签: Android studio如何生成aaAndroid studio aarAndroid 如何生成aar包 2016-12-21 14:42 1 ...
- oracle数据库的关闭
数据库停止: shutdown normal 无新连接 等待当前会话结束 等待当前事务结束 强制检查点并关闭文件(一致性关闭) shutdown transactional 无新连接 结束当前会话 等 ...
- [CQOI2015]任务查询系统 主席树_差分
Code: #include<vector> #include<cstdio> #include<algorithm> #include<string> ...
- DCDCBigBig's first blog @cnblogs~
其实初二末的时候我就在CSDN上开了博客(主要是为了存模板),但是无奈CSDN的页面真的太辣眼睛了…… 然后我就加入博客园欢快的大家庭啦!!!顺便膜拜巨佬学长%%%(我是蒟蒻不要喷我) 页面设置感谢x ...
- electron 新手教程 打包 exe
1.安装nodejs(会自动安装npm) 2.桌面新建文件夹 your-app (下面目录结构) your-app/ ├── package.json ├── main.js └── inde ...
- 紫书 习题 11-4 UVa 1660 (网络流拆点法)
这道题改了两天-- 因为这道题和节点有关, 所以就用拆点法解决节点的容量问题. 节点拆成两个点, 连一条弧容量为1, 表示只能经过一次. 然后图中的弧容量无限. 然后求最小割, 即最大流, 即为答案. ...
- 紫书 习题 8-20 UVa 1620 (找规律+求逆序对)
这道题看了半天没看出什么规律, 然后看到别人的博客, 结论是当n为奇数且逆序数为奇数的时候 无解, 否则有解.但是没有给出证明, 在网上也找到详细的证明--我也不知道是为什么-- 求逆序对有两种方法, ...
- sshd修改监听端口
vi /etc/sshd/sshd_config ListenAddress 0.0.0.0 #修改为 ListenAddress 192.168.0.1 #代表只监听192.168.0.1的SSH请 ...