转:JavaScript函数式编程(三)

作者: Stark伟

这是完结篇了。

在第二篇文章里,我们介绍了 Maybe、Either、IO 等几种常见的 Functor,或许很多看完第二篇文章的人都会有疑惑:

『这些东西有什么卵用?』

事实上,如果只是为了学习编写函数式、副作用小的代码的话,看完第一篇文章就足够了 kk:很贴近实际。第二篇文章和这里的第三篇着重于的是一些函数式理论的实践,是的,这些很难(但并非不可能)应用到实际的生产中,因为很多轮子都已经造好了并且很好用了。比如现在在前端大规模使用的 Promise 这种异步调用规范,其实就是一种 Monad(等下会讲到);现在日趋成熟的 Redux 作为一种 FLUX 的变种实现,核心理念也是状态机和函数式编程

一、Monad

关于 Monad 的介绍和教程在网络上已经层出不穷了,很多文章都写得比我下面的更好,所以我在这里只是用一种更简单易懂的方式介绍 Monad ,当然简单易懂带来的坏处就是不严谨,所以见谅/w\

如果你对 Promise 这种规范有了解的话,应该记得 Promise 里一个很惊艳的特性:

  1. doSomething()
  2. .then(result => {
  3. // 你可以return一个Promise链!
  4. return fetch('url').then(result => parseBody(result));
  5. })
  6. .then(result => {
  7. // 这里的result是上面那个Promise的终值
  8. })
  9. doSomething()
  10. .then(result => {
  11. // 也可以直接return一个具体的值!
  12. return 123;
  13. })
  14. .then(result => {
  15. // result === 123
  16. })

对于 Promise 的一个回调函数来说,它既可以直接返回一个值,也可以返回一个新的 Promise,但对于他们后续的回调函数来说,这二者都是等价的,这就很巧妙地解决了 nodejs 里被诟病已久的嵌套地狱。

事实上, Promise 就是一种 Monad,是的,可能你天天要写一大堆 Promise,可直到现在才知道天天用的这个东西竟然是个听起来很高大上的函数式概念。

下面我们来实际实现一个 Monad,如果你不想看的话,只要记住 『Promise 就是一种 Monad』 这句话然后直接跳过这一章就好了。

我们来写一个函数 cat ,这个函数的作用和 Linux 命令行下的 cat 一样,读取一个文件,然后打出这个文件的内容,这里 IO 的实现请参考上一篇文章:

  1. import fs from 'fs';
  2. import _ from 'lodash';
  3. var map = _.curry((f, x) => x.map(f));
  4. var compose = _.flowRight;
  5. var readFile = function(filename) {
  6. return new IO(_ => fs.readFileSync(filename, 'utf-8'));
  7. };
  8. var print = function(x) {
  9. return new IO(_ => {
  10. console.log(x);
  11. return x;
  12. });
  13. }
  14. var cat = compose(map(print), readFile);
  15. cat("file")
  16. //=> IO(IO("file的内容"))
  17. 由于这里涉及到两个 IO:读取文件和打印,所以最后结果就是我们得到了两层 IO,想要运行它,只能调用:
  18. cat("file").__value().__value();
  19. //=> 读取文件并打印到控制台

很尴尬对吧,如果我们涉及到 100 个 IO 操作,那么难道要连续写 100 个 __value() 吗?

当然不能这样不优雅,我们来实现一个 join 方法,它的作用就是剥开一层 Functor,把里面的东西暴露给我们:

  1. var join = x => x.join();
  2. IO.prototype.join = function() {
  3. return this.__value ? this.__value() : IO.of(null);
  4. }
  5. // 试试看
  6. var foo = IO.of(IO.of('123'));
  7. foo.join();
  8. //=> IO('123')

有了 join 方法之后,就稍微优雅那么一点儿了:

  1. var cat = compose(join, map(print), readFile);
  2. cat("file").__value();
  3. //=> 读取文件并打印到控制台

join 方法可以把 Functor 拍平(flatten),我们一般把具有这种能力的 Functor 称之为 Monad。

这里只是非常简单地移除了一层 Functor 的包装,但作为优雅的程序员,我们不可能总是在 map 之后手动调用 join 来剥离多余的包装,否则代码会长得像这样:

  1. var doSomething = compose(join, map(f), join, map(g), join, map(h));

所以我们需要一个叫 chain 的方法来实现我们期望的链式调用,它会在调用 map 之后自动调用 join 来去除多余的包装,这也是 Monad 的一大特性:

  1. var chain = _.curry((f, functor) => functor.chain(f));
  2. IO.prototype.chain = function(f) {
  3. return this.map(f).join();
  4. }
  5. // 现在可以这样调用了
  6. var doSomething = compose(chain(f), chain(g), chain(h));
  7. // 当然,也可以这样
  8. someMonad.chain(f).chain(g).chain(h)
  9. // 写成这样是不是很熟悉呢?
  10. readFile('file')
  11. .chain(x => new IO(_ => {
  12. console.log(x);
  13. return x;
  14. }))
  15. .chain(x => new IO(_ => {
  16. // 对x做一些事情,然后返回
  17. }))

哈哈,你可能看出来了,chain 不就类似 Promise 中的 then 吗?是的,它们行为上确实是一致的(then 会稍微多一些逻辑,它会记录嵌套的层数以及区别 Promise 和普通返回值),Promise 也确实是一种函数式的思想。

(我本来想在下面用 Promise 为例写一些例子,但估计能看到这里的人应该都能熟练地写各种 Promise 链了,所以就不写了0w0)

总之就是,Monad 让我们避开了嵌套地狱,可以轻松地进行深度嵌套的函数式编程,比如IO和其它异步任务。

二、函数式编程的应用

好了,关于函数式编程的一些基础理论的介绍就到此为止了,如果想了解更多的话其实建议去学习 Haskell 或者 Lisp 这样比较正统的函数式语言。下面我们来回答一个问题:函数式编程在实际应用中到底有啥用咧?

1、React

React 现在已经随处可见了,要问它为什么流行,可能有人会说它『性能好』、『酷炫』、『第三方组件丰富』、『新颖』等等,但这些都不是最关键的,最关键是 React 给前端开发带来了全新的理念:函数式和状态机。

我们来看看 React 怎么写一个『纯组件』吧:

  1. var Text = props => (
  2. <div style={props.style}>{props.text}</div>
  3. )

咦这不就是纯函数吗?对于任意的 text 输入,都会产生唯一的固定输出,只不过这个输出是一个 virtual DOM 的元素罢了。配合状态机,就大大简化了前端开发的复杂度:

  1. state => virtual DOM => 真实 DOM

在 Redux 中更是可以把核心逻辑抽象成一个纯函数 reducer:

  1. reducer(currentState, action) => newState

关于 React+Redux(或者其它FLUX架构)就不在这里介绍太多了,有兴趣的可以参考相关的教程。

2、Rxjs

Rxjs 从诞生以来一直都不温不火,但它函数响应式编程(Functional Reactive Programming,FRP)的理念非常先进,虽然或许对于大部分应用环境来说,外部输入事件并不是太频繁,并不需要引入一个如此庞大的 FRP 体系,但我们也可以了解一下它有哪些优秀的特性。

Rxjs 中,所有的外部输入(用户输入、网络请求等等)都被视作一种 『事件流』:

  1. ---> 用户点击了按钮 --> 网络请求成功 --> 用户键盘输入 --> 某个定时事件发生 --> ......

举个最简单的例子,下面这段代码会监听点击事件,每 2 次点击事件产生一次事件响应:

  1. var clicks = Rx.Observable
  2. .fromEvent(document, 'click')
  3. .bufferCount(2)
  4. .subscribe(x => console.log(x)); // 打印出前2次点击事件

其中 bufferCount 对于事件流的作用是这样的:

是不是很神奇呢? Rxjs 非常适合游戏、编辑器这种外部输入极多的应用,比如有的游戏可能有『搓大招』这个功能,即监听用户一系列连续的键盘、鼠标输入,比如上上下下左右左右BABA,不用事件流的思想的话,实现会非常困难且不优雅,但用 Rxjs 的话,就只是维护一个定长队列的问题而已:

  1. var inputs = [];
  2. var clicks = Rx.Observable
  3. .fromEvent(document, 'keydown')
  4. .scan((acc, cur) => {
  5. acc.push(cur.keyCode);
  6. var start = acc.length - 12 < 0 ? 0 : acc.length - 12;
  7. return acc.slice(start);
  8. }, inputs)
  9. .filter(x => x.join(',') == [38, 38, 40, 40, 37, 39, 37, 39, 66, 65, 66, 65].join(','))// 上上下下左右左右BABA,这里用了比较奇技淫巧的数组对比方法
  10. .subscribe(x => console.log('!!!!!!ACE!!!!!!'));

当然,Rxjs 的作用远不止于此,但可以从这个范例里看出函数响应式编程的一些优良的特性。

3、Cycle.js

Cycle.js 是一个基于 Rxjs 的框架,它是一个彻彻底底的 FRP 理念的框架,和 React 一样支持 virtual DOM、JSX 语法,但现在似乎还没有看到大型的应用经验。

本质的讲,它就是在 Rxjs 的基础上加入了对 virtual DOM、容器和组件的支持,比如下面就是一个简单的『开关』按钮:

  1. import xs from 'xstream';
  2. import {run} from '@cycle/xstream-run';
  3. import {makeDOMDriver} from '@cycle/dom';
  4. import {html} from 'snabbdom-jsx';
  5. function main(sources) {
  6. const sinks = {
  7. DOM: sources.DOM.select('input').events('click')
  8. .map(ev => ev.target.checked)
  9. .startWith(false)
  10. .map(toggled =>
  11. <div>
  12. <input type="checkbox" /> Toggle me
  13. <p>{toggled ? 'ON' : 'off'}</p>
  14. </div>
  15. )
  16. };
  17. return sinks;
  18. }
  19. const drivers = {
  20. DOM: makeDOMDriver('#app')
  21. };
  22. run(main, drivers);

当然,Cycle.js 这种『侵入式』的框架适用性不是太广,因为使用它就意味着应用中必须全部或者大部分都要围绕它的理念设计,这对于大规模应用来说反而是负担。

三、总结

既然是完结篇,那我们来总结一下这三篇文章究竟讲了些啥?

第一篇文章里,介绍了纯函数、柯里化、Point Free、声明式代码和命令式代码的区别,你可能忘记得差不多了,但只要记住『函数对于外部状态的依赖是造成系统复杂性大大提高的主要原因』以及『让函数尽可能地纯净』就行了。

第二篇文章,或许是最没有也或许是最有干货的一篇,里面介绍了『容器』的概念和 Maybe、Either、IO 这三个强大的 Functor。是的,大多数人或许都没有机会在生产环境中自己去实现这样的玩具级 Functor,但通过了解它们的特性会让你产生对于函数式编程的意识。

软件工程上讲『没有银弹』,函数式编程同样也不是万能的,它与烂大街的 OOP 一样,只是一种编程范式而已。很多实际应用中是很难用函数式去表达的,选择 OOP 亦或是其它编程范式或许会更简单。但我们要注意到函数式编程的核心理念,如果说 OOP 降低复杂度是靠良好的封装、继承、多态以及接口定义的话,那么函数式编程就是通过纯函数以及它们的组合、柯里化、Functor 等技术来降低系统复杂度,而 React、Rxjs、Cycle.js 正是这种理念的代言人,这可能是大势所趋,也或许是昙花一现,但不妨碍我们去多掌握一种编程范式嘛0w0 kk:长见识了

转:JavaScript函数式编程(三)的更多相关文章

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

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

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

    转:JavaScript函数式编程(一) 一.引言 说到函数式编程,大家可能第一印象都是学院派的那些晦涩难懂的代码,充满了一大堆抽象的不知所云的符号,似乎只有大学里的计算机教授才会使用这些东西.在曾经 ...

  3. JavaScript 函数式编程读书笔记1

    概述 这是我读<javascript函数式编程>的读书笔记,供以后开发时参考,相信对其他人也有用. 说明:虽然本书是基于underscore.js库写的,但是其中的理念和思考方式都讲的很好 ...

  4. 一文带你了解JavaScript函数式编程

    摘要: 函数式编程入门. 作者:浪里行舟 Fundebug经授权转载,版权归原作者所有. 前言 函数式编程在前端已经成为了一个非常热门的话题.在最近几年里,我们看到非常多的应用程序代码库里大量使用着函 ...

  5. JavaScript函数式编程(纯函数、柯里化以及组合函数)

    JavaScript函数式编程(纯函数.柯里化以及组合函数) 前言 函数式编程(Functional Programming),又称为泛函编程,是一种编程范式.早在很久以前就提出了函数式编程这个概念了 ...

  6. JavaScript 函数式编程读书笔记2

    概述 这是我读<javascript函数式编程>的读书笔记,供以后开发时参考,相信对其他人也有用. 说明:虽然本书是基于underscore.js库写的,但是其中的理念和思考方式都讲的很好 ...

  7. javascript函数式编程和链式优化

    1.函数式编程理解 函数式编程可以理解为,以函数作为主要载体的编程方式,用函数去拆解.抽象一般的表达式 与命令式相比,这样做的好处在哪?主要有以下几点: (1)语义更加清晰 (2)可复用性更高 (3) ...

  8. 在JavaScript函数式编程里使用Map和Reduce方法

    所有人都谈论道workflows支持ECMAScript6里出现的令人吃惊的新特性,因此我们很容易忘掉ECMAScript5带给我们一些很棒的工具方法来支持在JavaScript里进行函数编程,这些工 ...

  9. javascript函数式编程(一)

    一.引言 javascript函数式编程在最近两年来频繁的出现在大众的视野,越来越多的框架(react,angular,vue等)标榜自己使用了函数式编程的特性,好像一旦跟函数式编程沾边,就很高大上一 ...

随机推荐

  1. Oracle数据库中如何选择合适的索引类型 .

    索引就好象一本字典的目录.凭借字典的目录,我们可以非常迅速的找到我们所需要的条目.数据库也是如此.凭借Oracle数据库的索引,相关语句可以迅速的定位记录的位置,而不必去定位整个表. 虽然说,在表中是 ...

  2. 不同分辨率下获取不同js文件

    获取当前网站的目录  //js获取网站根路径(站点及虚拟目录),获得网站的根目录或虚拟目录的根地址 function getRootPath(){ //整个域名(如:http://vc3.cn/ind ...

  3. python实现简单表单校验框架

    # encoding=utf-8 from app.models import Student from flask import g import re from flask.ext.wtf imp ...

  4. KVO的实现原理探寻

    @import url(http://i.cnblogs.com/Load.ashx?type=style&file=SyntaxHighlighter.css);@import url(/c ...

  5. Ubuntu 12.04设置豆沙绿

    首先确保已安装dconf-editor sudo apt-get install dconf-tools     然后打开dconf-editor 找到:org – gnome -desktop – ...

  6. logstash Codec

    Logstash 使用一个名叫FileWatch的Ruby Gem库来监听文件变化,这个库支持glob扩展文件路径, 而且会记录一个叫.sincedb的数据库文件来跟踪被监听日志文件的当前读取位置,所 ...

  7. VC实现图片拖拽及动画

    基础知识 1.PictureBox控件的使用 2.加载位图文件 1.通过文件路径获得位图句柄 //获得位图句柄  void CMovePictureDlg::GetHandleFromPath(CSt ...

  8. [x-means] 1.x-means简介

    本文基于<X-means>和<BIC-notes>(原论文中BIC公式有误,这是对BIC的补充) K-means的缺点 每一轮迭代的计算花费大 需要用户指定K 易于收敛到局部最 ...

  9. 【UNIX网络编程(二)】基本TCP套接字编程函数

    基于TCP客户/server程序的套接字函数图例如以下: 运行网络I/O.一个进程必须做的第一件事就是调用socket函数.指定期望的通信协议类型. #include <sys/socket.h ...

  10. 《Effective C++》:条款46-条款47

    条款46请输入转换的时候,需要定义非模板成员函数 条款47请使用traits class表现类型信息 条款46:须要类型转换时请为模板定义非成员函数 条款 24提到过为什么non-member函数才有 ...