译者按: 近年来,函数式语言的特性都被其它语言学过去了。JavaScript异步编程中大显神通的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对象。

接下来,我们要做出重大改进,请注意!

迄今为止,所有的复合函数的两个参数都是函数,接下来我们把第一个参数改成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

Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了7亿+错误事件,得到了Google、360、金山软件、百姓网等众多知名用户的认可。欢迎免费试用!

版权声明

转载时请注明作者Fundebug以及本文地址:

https://blog.fundebug.com/2017/06/21/write-monad-in-js/

从函数式编程到Promise的更多相关文章

  1. angular2系列教程(六)两种pipe:函数式编程与面向对象编程

    今天,我们要讲的是angualr2的pipe这个知识点. 例子

  2. 转:JavaScript函数式编程(三)

    转:JavaScript函数式编程(三) 作者: Stark伟 这是完结篇了. 在第二篇文章里,我们介绍了 Maybe.Either.IO 等几种常见的 Functor,或许很多看完第二篇文章的人都会 ...

  3. 转: JavaScript函数式编程(二)

    转: JavaScript函数式编程(二) 作者: Stark伟 上一篇文章里我们提到了纯函数的概念,所谓的纯函数就是,对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环 ...

  4. 翻译连载 | 第 10 章:异步的函数式(上)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  5. 翻译连载 | 第 10 章:异步的函数式(下)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  6. 【大前端攻城狮之路】JavaScript函数式编程

    转眼之间已入五月,自己毕业也马上有三年了.大学计算机系的同学大多都在北京混迹,大家为了升职加薪,娶媳妇买房,熬夜加班跟上线,出差pk脑残客户.同学聚会时有不少兄弟已经体重飙升,开始关注13号地铁线上铺 ...

  7. redux源码解析-函数式编程

    提到redux,会想到函数式编程.什么是函数式编程?是一种很奇妙的函数式的编程方法.你会感觉函数式编程这么简单,但是用起来却很方便很神奇. 在<functional javascript> ...

  8. 给 JavaScript 开发者讲讲函数式编程

    本文译自:Functional Programming for JavaScript People 和大多数人一样,我在几个月前听到了很多关于函数式编程的东西,不过并没有更深入的了解.于我而言,可能只 ...

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

    转载请注明出处: http://hai.li/2017/03/27/prom... 背景 上篇文章 函数式JS: 一种continuation monad推导 得到了一个类似promise的链式调用, ...

随机推荐

  1. android启动画面隐藏状态栏全屏显示

    1.在根部局给一个id,然后直接设置就行了layout.setSystemUiVisibility(View.INVISIBLE); 状态栏就没有了. 2.如果你只是想改变状态栏颜色的也可以 //5. ...

  2. SDWebImage之UIView+WebCache

    UIView+WebCache是我们能很方便的使用sd_setImageWithURL:系列方法来加载图片的关键类.UIButton(WebCache).MKAnnotationView(WebCac ...

  3. MyBatis 源码分析 - 内置数据源

    1.简介 本篇文章将向大家介绍 MyBatis 内置数据源的实现逻辑.搞懂这些数据源的实现,可使大家对数据源有更深入的认识.同时在配置这些数据源时,也会更清楚每种属性的意义和用途.因此,如果大家想知其 ...

  4. 腾讯Alloy团队代码规范

    概述 我个人很看重代码规范,因为代码是写给别人看的,按规范写别人才更容易理解.之前苦于没有代码规范的资料,现在在github上面看到了腾讯Alloy团队的代码规范,于是学习了一下,并记录下我自己还没怎 ...

  5. Servlet案例7:jsp技术及案例

    jsp运行原理: 根据jsp文件创建java文件(servlet),并编译运行 第一次访问时会被翻译成servlet后执行 jsp三个指令: 1.page指令:页面翻译运行的属性的配置(通常使用默认) ...

  6. OpenStack-Ocata版+CentOS7.6 云平台环境搭建 — 2.安装配置OpenStack基础服务

    节点配置情况说明: 控制节点:controller: IP:192.168.164.128 hostname&hosts:likeadmin 计算加点:Nova: IP:192.168.164 ...

  7. shell 中的三种引号的作用

    1. 单引号(' ') 单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的: 单引号字串中不能出现单独一个的单引号(对单引号使用转义符后也不行),但可成对出现,作为字符串拼接使用. 2. 双 ...

  8. Linux下用ifconfig命令设置IP、掩码、网关

    设置IP和掩码ifconfig eth0 192.168.1.240 netmask 255.255.255.0设置网关route add default gw 192.168.1.1 每日一言:靡不 ...

  9. 08-部署node节点

    部署kubernetes node节点 kubernetes node 节点包含如下组件: Flanneld: 省略,参照之前部署的文档 Docker1.12.5: 省略,参照之前部署的文档 kube ...

  10. Lock接口

    Lock与synchronized Lock和synchronized在功能上是一样的.不过Lock提供了一些其他功能,包括定时的锁等待.可中断的锁等待.公平性,以及实现非块结构的加锁. 从性能上Lo ...