资讯专栏INFORMATION COLUMN

函数式JS: 原来promise是这样的monad

ZweiZhao / 2573人阅读

摘要:定义这是类型签名的表述。实际上对应着,只是在里作为立即量传入,在和的返回值中作为闭包引用传入。同时根据看出返回值是用回调返回值的。的输出是的包裹。的方法借助了闭包引用额外输入了,而输入的函数输入是输出则是借助实现的。

转载请注明出处: 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类型的值,可以这么理解:

左单位元法则就是将包裹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

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}

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

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

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返回

这里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)})
    }
}

如果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 })
    }
}

为了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改成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

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

Monads by Diagram

Monads and Gonads

Monad laws

A Fistful of Monads

Javascript Functor, Applicative, Monads in pictures

Functors and Applicatives

JS函数式编程指南

Translation from Haskell to JavaScript of selected portions of the best introduction to monads I’ve ever read

Flipping arrows in coBurger King

My angle on MonadsBackground

Awesomely descriptive JavaScript with monads

Monads in JavaScript

怎样用简单的语言解释 monad?

如何解释 Haskell 中的单子?

How do I return the response from an asynchronous call?

Promise

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/82234.html

相关文章

  • Js-函数编程

    摘要:组合组合的功能非常强大,也是函数式编程的一个核心概念,所谓的对过程进行封装很大程度上就是依赖于组合。在理解之前,先认识一个东西概念容器容器为函数式编程里普通的变量对象函数提供了一层极其强大的外衣,赋予了它们一些很惊艳的特性。 前言 JavaScript是一门多范式语言,即可使用OOP(面向对象),也可以使用FP(函数式),由于笔者最近在学习React相关的技术栈,想进一步深入了解其思想...

    whinc 评论0 收藏0
  • JavaScript 异步

    摘要:从最开始的到封装后的都在试图解决异步编程过程中的问题。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。写一个符合规范并可配合使用的写一个符合规范并可配合使用的理解的工作原理采用回调函数来处理异步编程。 JavaScript怎么使用循环代替(异步)递归 问题描述 在开发过程中,遇到一个需求:在系统初始化时通过http获取一个第三方服务器端的列表,第三方服务器提供了一个接口,可通过...

    tuniutech 评论0 收藏0
  • 你造 Promise Monad

    摘要:但有时候,所有的鸟都会想要停在同一边,皮尔斯就失去了平衡,就会让他从钢索上掉下去。我们这边假设两边的鸟差异在三个之内的时候,皮尔斯仍能保持平衡。 Monad 这个概念好难解释, 你可以理解为一个 Lazy 或者是状态未知的盒子. 听起来像是薛定谔猫(估计点进去你会更晕了). 其实就是的, 在你打开这个盒子之前, 你是不知道里面的猫处在那种状态. Monad 这个黑盒子, 里面到底卖的神...

    张率功 评论0 收藏0
  • 【响应编程思维艺术】 (3)flatMap背后代数理论Monad

    摘要:本文是响应式编程第二章序列的深入研究这篇文章的学习笔记。函数科里化的基本应用,也是函数式编程中运算管道构建的基本方法。四资料参考函数式编程指南 本文是Rxjs 响应式编程-第二章:序列的深入研究这篇文章的学习笔记。示例代码托管在:http://www.github.com/dashnowords/blogs 更多博文:《大史住在大前端》目录 showImg(https://segme...

    MorePainMoreGain 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<