译者按: 近年来,函数式语言的特性都被其它语言学过去了。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. open:在终端打开文件|应用程序等

    1. 需求&解决方案 打开文件 或url(用默认应用程序): $ open fileName 打开文件 或url(用指定应用程序): $ open -a appName fileName ## ...

  2. C# Autofac集成之Framework WebAPI

    Web API 2集成需要Autofac.WebApi2 NuGet包. Web API集成需要Autofac.WebApi NuGet包. Web API集成为控制器,模型绑定器和操作过滤器提供了依 ...

  3. Zhu-Takaoka Two-dimensional Pattern Matching

    Two dimensional pattern matching. Details may be added later.... Corresponding more work can be foun ...

  4. 第二十一节:Java语言基础-关键字,标识符,注释,常量和变量,运算符

    Java语言基础-关键字,标识符,注解,常量和变量,运算符 class Demo { public static void main(String[] args){ System.out.printl ...

  5. JS应用实例6:二级联动

    本案例很常用,应用场景:注册页面填写籍贯,省市二级联动 总体思想:创建一个二维数组存入省市,获取选中的省份并比较,创建标签遍历添加 代码: <!DOCTYPE html> <html ...

  6. Java匹马行天下之JavaSE核心技术——Java基础语法

    Java基础语法 一.   认识Java 1. Java 简介 java 是一种高级的面向对象的程序设计语言,使用Java语言编写的程序时跨平台的.从pc到手机,都有Java开发的程序和游戏,Java ...

  7. 双系统Ubuntu无法访问Win10磁盘分区解决方法

    今天在linux下打开win的NTFS硬盘总是提示出错了,而且是全部的NTFS盘都出错,其中sda3错误显示如下: Error mounting /dev/sda3 at /media/struggl ...

  8. Python - 使用Pyinstaller将Python代码生成可执行文件

    1 - Pyinstaller简介 Home-page: http://www.pyinstaller.org PyInstaller是一个能够在多系统平台(Windows.*NIX.Mac OS)上 ...

  9. 【不遮遮掩掩】Github上传本地代码以及常见问题解决方案

    2019.20.18更新: 把完整命令打一遍吧,不然看完太累了不是 //初始化文件夹 git init //初始化目录,把基本文件下载下来,如ignore文件 git pull --rebase or ...

  10. 如何将页面上的数据导入excel中

    网上关于页面数据导入excel的文章很多,但是大部分都是关于 ActiveXObject 对象,可是ActiveXObject 对象是只支持IE的,可我连IE11也测试了,还是无法识别,又查到消息,好 ...