本文源于我在组内的一次关于函数式的分享,整理成此文。

近来的前端技术趋势中,从 React 无状态组件到 Redux 单向数据流,到处都能发现函数式的相关影子。了解函数式相关的基础知识对我们理解这些流行元素很有必要。

先看下面这张比较有趣的图,最近一段时间,twitter 上这张图很火。当然,这个图主要还是针对 NPM 和函数式之滥用,但侧面说明了函数组合的强大。这里我们并不是为了做什么都要用函数式去套,要那样就失去了其本身的意义了。

NPM 之泛滥

当我们的代码越来越复杂以后,一般都会遇到下面这些问题:

  • 状态错综复杂,不容易追踪 BUG
  • 不易于测试
  • 难以维护

此时,我们需要利用函数式的思想对复杂的代码抽象化。什么是函数式呢?维基百科给出的解释如下:

In computer science, functional programming is a programming paradigm — a style of building the structure and elements of computer programs — that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.

从上面看得出,函数式程序主要有三个特点:

  1. 声明式
  2. 纯函数(不可变数据结构)
  3. 无状态(输入不依赖任何中间过程,对之前操作的忽视,只关注输入是什么)

『UNIX 编程艺术』告诉了我们 UNIX 设计的原则,即所谓的 KISS(Keep It Simple and Stupid)。使用函数式之后,因为我们需要将任务进行分解,代码的结构会非常清晰,各功能函数职责也会非常清晰。这与 KISS 原则不谋而合。

前面提到了函数式程序有一个重要的特点,即声明式。还是以一个简单的例子作对比吧:求一个数组各元素的平方和。

过程式的代码也许我们会这么写:

1
2
3
4
5
6
7
let sumOfSquares = function(list) {
var result = 100;
for (var i = 0; i < list.length; i++) {
result += square(list[i]);
}
return result;
};

使用声明式的方法,我们会更钟爱于使用函数组合的方式来书写:

1
2
3
4
5
let sumOfSquares = function (list) {
return list.reduce(function (prev, val) {
return prev + square(val);
}, 100);
}

更抽象地,我们可以这么来写:

1
2
let add = (x, y) => x + y;
let sumOfSquares = pipe(map(square), reduce(add, 0));

是不是会觉得其代码简洁很多呢。

另外,在我们日常的开发过程中,面向对象(OO)的编程思想往往扮演着一个很重要的角色,与这里的函数式有什么区别呢?

面向对象以类、继承为其根基,强调的是数据与方法的统一封装,即,通过将接口的实现封装在类中,以接口继承和组合数据的方式达到扩展的目的。所以,其核心是类,是对象,是数据。

但函数式则与它不同,它强调的是函数,推崇数据与函数的分离,对于函数,不强调其具体的实现方式。通过函数的组合达到扩展的目的。

我们提到函数式程序中,函数都是纯函数,什么是纯函数呢?

从标准的解释来看,纯函数不改变外部的状态,只依赖于输入,对于同样的输入,永远都只有同样的输出。比如,随机函数就不是纯函数,而 Array.slice 就是一个纯函数,因为它并不直接在原数组的基础进行修改,而是重新返回了一个新的数组,对应于纯函数;与之相反,Array.splice 就会在原数组基础上进行修改,对应的即非纯函数。

纯函数的好处在于,如果代码有 BUG,我们几乎不需要想方设法地去构造其重现环境,因为它是必现的;对于单元测试来说,我们也无需要设置各种前置后置条件,因为直接测这个纯函数就好了。另外,React 中的代码热替换功能能够有效运行,很重要的一点是,render 是一个纯函数。

到现在,函数式程序相较于其它的程序优点就很明显了:

  1. 易于单元测试
  2. 易于调试(同一输入意味着相同的输出,能必现)
  3. 并发执行,无需考虑锁机制

前面提到,函数式编程通过组合函数的方式来达到扩展的目的。那么,什么是函数的组合呢?

在涉及函数组合前,需要先了解柯里化(curry)这个概念。

curry

当然,我们这里说的 curry 不是吃的食物,而是指一种编程的技法,以纪念 Haskell Curry 而命名,还是看看函数柯里化的官方解释吧。

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数 (最初函数的第一个参数) 的函数,并且返回接受余下的参数且返回结果的新函数的技术。

下面这段代码就是函数的柯里化:

1
2
var add = curry((a, b) => (a + b));
var add10 = add(10);

那么,柯里化有什么作用呢?可以看下面这个例子:

1
2
3
4
5
6
7
8
9
10
var get = curry(function(property, object) {
return object[property];
})
var map = curry(function (fn, mappable) {
return mappable.map(fn);
})
// 声明式
var getIDs = map(get('id'))
getIDs(list); // [1, 2, 3]

TODO: pipe & compose

参考:file:///Users/xuanfeng/research/functional/slide/reveal.js/functional-programming.html