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接收fg两个参数,然后返回值是一个函数。该函数接收一个参数x, 先将函数g作用到x, 其返回值作为另一个函数f的输入。

addOneThenMul3是我们通过composeF定义的一个新的函数:由mul3add1复合而成。

接下来看另一个实际的例子:我们有两个文件,第一个文件存储了第二个文件的路径,第二个文件包含了我们想要取出来的内容。使用刚刚定义的复合函数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 有什么性质呢?

  1. 它有一个环境;
  2. 这个环境里面不一定有值;
  3. 提供一个获取该值的方法;
  4. 有一个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 ]

我们可以验证:

  1. [] 是环境
  2. []可以为空,值不一定存在;
  3. 通过forEach可以获取;
  4. 我们定义了flatMap来作为bind函数。

结论

  • Monad是回调函数 ?
    根据性质3,是的。
  • 回调函数式Monad?
    不是,除非有定义bind函数。

欢迎加入我们Fundebug全栈BUG监控交流群: 622902485

版权声明:
转载时请注明作者Fundebug以及本文地址:
https://blog.fundebug.com/2017/06/21/write-monad-in-js/
时间: 2024-10-11 18:25:06

Promise是Monad吗?的相关文章

从源码看 Promise 概念与实现

Promise 是 JS 异步编程中的重要概念,它较好地解决了异步任务中回调嵌套的问题.在没有引入新的语言机制的前提下,这是如何实现的呢?上手 Promise 时常见若干晦涩的 API 与概念,它们又为什么存在呢?源码里隐藏着这些问题的答案. 下文会在介绍 Promise 概念的基础上,以一步步代码实现 Promise 的方式,解析 Promise 的实现机制.相应代码参考来自 PromiseJS 博客 及 You don't know JS 的若干章节. Why Promise (有使用 Pr

future and promise

Future and Promise Future is a sort of placeholder object that you create for a result that does not exist. Generally, the result of the Future is computed concurrently and can be later collected. Composing concurrent tasks in this way tends to faste

Monad / Functor / Applicative 浅析

前言 Swift 其实比 Objective-C 复杂很多,相对于出生于上世纪 80 年代的 Objective-C 来说,Swift 融入了大量新特性.这也使得我们学习掌握这门语言变得相对来说更加困难.不过一切都是值得的,Swift 相比 Objective-C,写出来的程序更安全.更简洁,最终能够提高我们的工作效率和质量. Swift 相关的学习资料已经很多,我想从另外一个角度来介绍它的一些特性,我把这个角度叫做「烧脑体操」.什么意思呢?就是我们专门挑一些比较费脑子的语言细节来学习.通过「烧

Functional Programming without Lambda - Part 2 Lifting, Functor, Monad

Lifting Now, let's review map from another perspective. map :: (T -> R) -> [T] -> [R] accepts 2 parameters, a function f :: T -> R and a list list :: [T]. [T] is a generic type paramterized by T, it's not the same as T, but definitely shares s

从函数式编程到Promise

译者按: 近年来,函数式语言的特性都被其它语言学过去了.JavaScript异步编程中大显神通的Promise,其实源自于函数式编程的Monad! 原文: Functional Computational Thinking?-?What is a monad? 译者: Fundebug 为了保证可读性,本文采用意译而非直译.另外,本文版权归原作者所有,翻译仅用于学习. 如果你使用函数式编程,不管有没有用过函数式语言,在某总程度上已经使用过Monad.可能大多数人都不知道什么叫做Monad.在这篇

异步链式编程—promise沉思录

一.promise的组成 1.task:promise要完成的任务: 2.result:处理完的数据: 3.status:状态: 4.fulfill.reject(对应catch) 5.ResolveCallback ErrorCallback promise状态的解释函数 6.resolve: 对promise当前的状态作出解释,已完成的状态立即执行回掉,未完成的状态注册回掉函数: 7.then:前一promise的回掉注册,后一promise的前导: 二.promose状态机: fulfil

promise方法

promise TemplateService.uploadTempate(fieKey).then(function(result){sef.memKay = result.data},function(error){console.log(error)}) 只要有then,它就是一个promise. TemplateService.uploadTempate(fieKey)返回一个promise,承诺如果TemplateService.uploadTempate(fieKey)函数执行成功,

Promise 原理探究及其简单实现

可移步 http://donglegend.com/2016/09/11/promise%E5%8E%9F%E7%90%86%E6%8E%A2%E7%A9%B6/ 观看 Promise是个什么玩意,大家都知道,度娘告诉我,以同步方式书写异步,解决回调地狱... 状态机 早闻Promise的大名,简单介绍,根据状态改变来执行相应处理函数.Promise的状态极其简单,只有 “pending”, “resolved”, “rejected”三种状态然后就是如何实现的问题,最关键的当然是监听到状态的更

【Mocha.js 101】同步、异步与 Promise

前情提要 在上一篇文章<[Mocha.js 101]Mocha 入门指南>中,我们提到了如何用 Mocha.js 进行前端自动化测试,并做了几个简单的例子来体验 Mocha.js 给我们带来的便利. 在本篇文章中,我们将了解到 Mocha.js 的同步/异步测试,以及如何测试 Promise. 同步代码测试 在上一篇文章中,其实我们已经学会了如何测试同步代码.今天,我们 BDD 风格编写一个测试: var should = require( 'should' ); var Calculator