转载请注明出处: 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是一般的值,fg是一般的函数,monad是一个Monad类型的值,可以这么理解:

  1. 左单位元法则就是将包裹unit(x)和函数f传给flatMap执行等价于将包裹中的值x抽出传给函数f执行

  2. 右单位元法则就是将包裹monad和函数unit传给flatMap执行等价于包裹monad本身(有点像1*1=1

  3. 关联性法则就是将包裹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

unitflatMap的特性可以直观地对应为

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)

这样如果unitflatMap这两个直接函数可以构造推导出来,就可以窥探Promise的真面目了。同学们!这道题!必考题!头两年不考,今年肯定考!

构造推导unit

function unit(f){ return f}
  1. flatMap :: Monad a -> (a -> Monad b) -> Monad bflatMap(unit(g0))(g1)可知传入g1的参数就是a,对应着"0"

  2. 但由unit :: a -> Monad aunit(g0)得到的a却对应着g0。实际上a对应着"0",只是ag0里作为立即量传入,在g1g2的返回值中作为闭包引用传入。

  3. 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
})
}
}
  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 }}
  2. 实际flatMap做了3步工作

    1. 解包ma取出a

    2. a传到g1中执行

    3. 将执行结果b包裹成mb返回

  3. 这里mag1都是容器,通过回调得到输出结果,所以在ma的回调中执行g1(a),再在g1(a)的回调中得到执行结果v,再将执行结果v赋值给外部变量b,最后将bunit包裹成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)})
    }
    }
  4. 如果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.3
    function flatMap(ma){
    return function(g1) {
    var b;
    ma(function(a) {
    g1(a)(function(v) {
    b(v)
    })
    });
    return unit(function(c) { b = c })
    }
    }
  5. 为了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) })
    }
    }
  6. 整合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
    })
    }
    }
  7. 由于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
    })
    }
    }
  8. 现在可以测试下代码了

    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改成newPromiseflatMap改成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
  1. newPromise的输入是一个函数,但在推导构造unit里解释过,这里借助了立即量和闭包引用来额外增加了输入anewPromise的参数则作为构造unit的补充逻辑。

  2. newPromise的输出是a的包裹Monad a

  3. newPromise的方法then借助了闭包引用额外输入了Monad a,而输入的g函数输入是a输出则是借助newPromise实现的Monad b

  4. 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) }) //关联性
  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 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
  2. 右单位元法则验证代码如下:

    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
  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
    }
    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的更多相关文章

  1. 函数式 js 接口实现原理,以及 lodash/fp 模块

    函数式 js 接口 之前在 youtube 上看到一个技术视频,讲“underscore.js的接口为什么不好用”,以及什么样的接口更好用.演讲者是 lodash.js 的作者,他提出了一种“全面函数 ...

  2. 关于学习js的Promise的心得体会

    最近一直在研究js的Promise对象,其中有一篇blog写得比较通俗易懂,转发如下: http://www.cnblogs.com/lvdabao/p/es6-promise-1.html 参照上面 ...

  3. 从函数式编程到Promise

    译者按: 近年来,函数式语言的特性都被其它语言学过去了.JavaScript异步编程中大显神通的Promise,其实源自于函数式编程的Monad! 原文: Functional Computation ...

  4. js的Promise学习笔记(1)

    1: 何为Promise Promise是抽象异步处理对象以及对其对象进行各种操作的组件,是基于并列/并行处理设计的一种编程语言. 说到基于JavaScript的异步处理,大多数都会想到利用回调函数. ...

  5. 讲解JS的promise,这篇是专业认真的!

    http://www.zhangxinxu.com/wordpress/2014/02/es6-javascript-promise-%E6%84%9F%E6%80%A7%E8%AE%A4%E7%9F ...

  6. Angular JS中 Promise用法

    一.Promise形象讲解A promise不是angular首创的,作为一种编程模式,它出现在1976年,比js还要古老得多.promise全称是 Futures and promises. 而在j ...

  7. Node.js之Promise维护(同步)多个回调(异步)状态

    金天:学习一个新东西,就要持有拥抱的心态,如果固守在自己先前的概念体系,就会有举步维艰的感觉..NET程序员初用node.js最需要适应的就是异步开发, 全是异步,常规逻辑下遍历列表都是异步,如何保证 ...

  8. Node.js之Promise

    2015年发布了ES6标准,所谓 Promise,就是ES6标准的一个对象,用来传递异步操作的消息.它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可供进一步 ...

  9. JS 中Promise 模式

    异步模式在web编程中变得越来越重要,对于web主流语言Javscript来说,这种模式实现起来不是很利索,为此,许多Javascript库(比如 jQuery和Dojo)添加了一种称为promise ...

随机推荐

  1. QStandardItemModel的data线程安全(在插入数据时,临时禁止sizeHint去读model中的data)

    版权声明:本文为博主原创文章,欢迎转载,转载请注明出处 https://blog.csdn.net/MatchYang/article/details/52988257 在直接使用QStandardI ...

  2. @Query Annotation in Spring Data JPA--转

    原文地址:http://javabeat.net/spring-data-jpa-query/ In my previous post on Spring Data, I have explained ...

  3. SqlServer存储过程加密与解密

    ★ 加密存储过程 ★: IF EXISTS (SELECT name FROM sysobjects WHERE name = 'encrypt_this' AND type = 'P')   DRO ...

  4. Batch脚本获取日期SET YEAR=%date:~10,4%

    在batch脚本中我们可以通过下面的语句来对日期进行操作: SET YEAR=%date:~10,4% SET MONTH=%date:~4,2% SET DAY=%date:~7,2% SET HO ...

  5. Python的Flask框架入门-Ubuntu

    全文请见tuts code:An Introduction to Python's Flask Framework Flask是Python一个小而强大的web框架.学起来简单,用起来也容易,能够帮你 ...

  6. 编程语言与Python学习(一)

    1.1 编程与编程语言 1.1.1 编程语言 计算机的发明,是为了用机器解放人力,而编程的目的则是将人类的思想流程按照某种能够被计算机识别的表达方式传递给计算机,从而达到让计算机能够像人脑一样自动执行 ...

  7. ubuntu上配tensorflow

    前一阵绕了大弯路,基本弄好了UEFI双系统后,面对的就是CUDA咋装在Linux. 好在教程好多,有些朋友建议先装CUDA再装显卡驱动.弄好之后记录一下详细过程吧. *** 这两天看了一些教程,还是感 ...

  8. Windows环境下使用Guard整合Compass和Livereload进行SASS的开发

    配置运行环境 Guard,Compass 和 Livereload 是 Ruby 的 Gem 套件,需要 Ruby 运行环境.另外还需要安装 Ruby 的扩展开发包 Development-Kit,以 ...

  9. 一袭白衣一 IDEA的破解安装以及汉化

    DEA是一款比eclipse用起来更好用的一款代码编辑器,本人之前也是一直在用eclipse来写代码,后来发现了IDEA用起来会更顺手,所以又转用IDEA了,今天给大家分享一下IDEA的下载安装破解以 ...

  10. for 的相关用法

    forEach() <!DOCTYPE html> <html> <head> <meta charset="utf-8"> < ...