# 伍.2.1 什么是函数式编程？

## 01.程序的本质

程序是什么？看一张图：

![](https://1680353414-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LnlixiOMXyluGpXDGGs%2F-M5tFgYSCXIFr4Pu9dtV%2F-M5tICm6XK9LN5RkMzHO%2F6.3.1.0.1.png?alt=media\&token=1093178d-3758-48ba-abe8-006af9f67dc2)

程序由输入数据、运算、输出结果三部分组成。输入与输出属于I/O，都有成熟的代码去实现了。而运算那部分会根据需求而千变万化，这部分是可以独立出来的。如果运算里面包括I/O的代码和逻辑，就应该挪走到输入数据、输出结果那边。

那么，如果把运算独立出来，会是什么效果？嗯，至少是让三个部分之间耦合更少一点了，方便代码组织和维护。

而把程序的运算部分独立出来，既可以使用函数式编程，又可以使用命令式编程来实现。

笔者对命令式编程（面向对象编程主要为命令式编程）比较熟悉，也对函数式编程有所实践，综合考虑之后，认为函数式编程实现程序的运算部分会更畅快一些。

## 02.函数式编程的定义

**函数式编程是以调用函数为主的软件开发风格。**

没错，我们可能很早就开始使用函数了，但是，别以为那就是函数式编程的全部了。

> 函数式编程会改变我们处理日常工作时的思路，让我们使用函数来**抽象**作用在数据之上的控制流与操作，从而在系统中消除副作用并减少对状态的改变。

请注意我重点标识了“抽象”二字，其实编程功力比拼到最后就是拼抽象能力了。虽然上面这段话可能不好理解，但没关系，后面会慢慢展开阐述。

## 03.函数式编程与命令式编程

函数式编程是相对命令式编程而言的，二者都属于编程范式，最大的不同在于：

**命令式编程关心解决问题的步骤，函数式编程关心数据的映射。**

所谓命令式编程，也就是说你要做什么事情，得把达到目的的步骤详细描述出来，然后交给机器去运行。这也正是命令式编程的理论模型“图灵机”的特点，一条写满数据的纸带，一条根据纸带内容运动的机器，机器每动一步都需要纸带上写着如何达到。

来看一个例子：统计小组5个学生体重之和。即使是刚入门的前端工程师也可以很快写出如下代码：

```javascript
let studentsWeight=[49,50,43,55,64];//存放每个学生的体重

let sum=0;
for(let i=0;i<studentsWeight.length;i++){
    sum+=studentsWeight[i];
}
console.log(sum);//>> 261
```

通过一个for循环，然后引入临时变量studentsWeigth和sum，让sum与每个学生体重相加，最后打印结果。这就是典型的命令式编程，你要做什么事情，得把达到目的的步骤详细的描述出来，然后交给机器去运行。在这个过程中，引入了临时变量studentsWeigth和sum，并且for循环一次，sum的值便被改变一次，这些都是**副作用**。

那么，不用这种方式，函数式编程会如何做呢？如下：

```javascript
console.log([49,50,43,55,64].reduce((a,b)=>a+b));//>> 261
```

哇，惊人的简洁清爽！上面的这段代码主要表达要实现什么（调用`reduce`函数求和），而不是在描述具体的实现的步骤，也没有循环的代码。具体的细节已经封装到`reduce`函数里面，我们看不到也暂时不用关心。这样就可以把程序员注意力从程序的步骤控制、变量的维护等繁琐的事情中解放出来，去专注于要实现什么。而且中间没有副作用。

> 关于“副作用”的更多阐释，引用自《Java函数式编程》：
>
> 当我说“没有副作用”的时候，我是指没有可观测到的副作用。函数式的程序是由接收参数并返回值的函数复合而成的，仅此而已。你并不关心函数内部发生了什么……但是在实际上，程序是为完全非函数式的计算机而编写的。所有的计算机都基于相同的命令式范式，所以函数就是如下黑盒：\
> ■ 接收一个（单独的）参数。\
> ■ 内部做一些神秘的事情，例如改变变量的值，还有许多命令式风格的东西，但是在外界来看并没有什么东西。\
> ■ 返回一个（单独的）值。\
> 这只是理论。实际上，函数不可能完全没有副作用。函数会在某个时候返回一个值，而这个值可能是变化的。这就是一个副作用。它可能会造成一个内存耗尽的错误，或者是堆栈溢出的错误，导致应用程序崩溃，这在某种意义上就是一个可观测到的副作用。并且它还会造成写内存、寄存器变化、加载线程、上下文切换和其他确实会影响外界观测的这类事情。\
> 所以函数式编程其实是编写非故意的副作用的程序，我的意思是，副作用是程序预期结果的一部分。非故意的副作用也应该越少越好。

那么，数据的映射又是什么意思呢？还是上面的例子，如果要求给每个学生体重单位由公斤变成克，函数式编程该怎么弄呢？如下代码：

```javascript
[49,50,43,55,64].map((a)=>a*1000);// [49000, 50000, 43000, 55000, 64000]
```

这段代码体现的思维，就是**旧体重数组**到**新体重数组**的**映射。在这里，映射就是由函数`map`和它的参数（也是一个函数）体现，不然如何实现旧数组到新数组的映射呢？那么，前面说的“函数式编程关心数据的映射”，其实就是关心函数（有点废话 😄 ，但能方便理解）。**

如果看到这里还是云里雾里的话，别着急，可能因为我们对函数式编程的一些基础概念没有了解，接着往下看。

## 04.纯函数

所谓的纯函数就是，\*\*对于相同的输入，永远会得到相同的输出，而且没有任何可观察的副作用，也不依赖外部环境的状态。\*\*比如下面代码：

```javascript
let arr=[1,2,3,4,5];
arr.slice(0,2);//这是纯函数，因为只要参数一样，输出总一样
arr.slice(0,2);//输出结果与上一句相同

arr.splice(0,2);//非纯函数，参数一样，但是输出变化了
arr.splice(0,2);//输出变化了，与上一句结果不同
```

另外`Date.now()`也是不纯的，他总是依赖时间来返回不同的结果。

纯函数是函数式编程至关重要的前提，也即函数的输出只能取决于函数的参数输入。

我们写代码的时候，可能会因为代码严重依赖环境，有时候调用同一个函数，输入相同的参数，导致结果居然不同，更不用说由此带来的各种副作用了。

而采用函数式编程，确定的输入决定了确定的输出，这意味着只要参数对了，结果一定在预期中。也就是说，**函数式编程没有无法重现的bug**！

“啥意思？说人话！”

“举个例子，假设你的程序里统共有ABCDEFG函数，其中ABCDEF是纯函数，而函数G是不纯的。若ABCDEF调试不通过，bug在任何时候是必现的；若ABCDEF调试通过，则你的程序出了bug，只有可能是G出了问题，如此可简单粗暴地快速定位问题所在。”

纯函数的代码写出来，只有通过与不通过，没有中间状态。在这样的前提下，单元测试相对容易实现，能极大地增强程序员的代码自信。想象一下，你自己的纯函数的代码若调试通过了，交上去就几乎是无bug的成果，内心会多么自豪！

## 05.柯里化（Currying）

关于柯里化的基础知识，请回顾“[JavaScript函数柯里化](../../id-1/3.-qi-ta-zhi-shi-dian/1.3.2)”。

那么柯里化和函数式编程又有什么联系呢？等你看完下面的“函数组合”我再来回答这个问题。

## 06.函数组合（Compose）

知道了纯函数和柯里化之后，我们可能会写出这样的“包菜式”代码：

```javascript
fn1(fn2(fn3(x)));
```

虽然这的确是函数式编程风格的，但它某种意义上是“丑陋”的。为了解决这种包菜式的函数嵌套的问题，我们需要用到“函数组合”。

```javascript
//将多个函数组合为一个
let compose = (fn1, fn2) => (x => fn1(fn2(x)));

let add3 = x => x + 3;
let mul2 = x => x * 2;

compose(mul2, add3)(2); //>> 10 
```

上面的compose可以把任何两个纯函数结合到一起。当然你也可写出组合3个、4个……函数的compose代码。这里我曾经看到过一段比较巧妙组合函数的实现代码：

```javascript
function compose(...fns){
    return function(x){
        return fns.reduce(function(arg,fn){
            return fn(arg);
        },x);
    }
}

let add3 = x => x + 3;
let mul2 = x => x * 2;

compose(mul2, add3)(2); //>> 7  

// 思考一下为什么是7而不是10？如何改造让结果是10？
```

注意参与组合的函数必须是纯函数。因为如果不纯，那就有副作用，组合前后的结果可能不一样。

这样，我们便可以把各种微小功能的纯函数组合起来，形成一个功能强大的操作。

到这，我们应该明白了，因为柯里化可以把多个参数精简为传一个参数的函数，也就是说，多参数的函数，精简到最后，都可以只传一个参数，这之后就可以帮助实现函数组合（上面例子中，组合后的函数都统一只传了一个参数），以便像搭积木一样的将微小纯函数组合起来实现功能强大的操作。

“喔喔，原来这就是柯里化的妙用之一？”

“是呀！”

## 07.函子（Functor）

### 定义

在函数式编程中，Functor（函子）可以说是一个很基础的概念。

当然了，还有一个更基础的概念是函数(function)。在函数式编程的世界里，函数它实际上表达的是一个对象到另一个对象的映射关系（我在这里使用了对象一词，因为作为一个Java程序员，对象可以说是最熟悉的概念）。上面提到的第一个对象，指的是函数的输入参数；而另外一个对象，则是指函数返回的结果；映射关系指的是函数。

> 函数处理的是对象，而函子则处理的是范畴。

所谓“范畴”，原本来自“范畴论”，从编程的角度来理解，我们可以理解成高阶对象。什么是“高阶对象”呢？ 实际上就是一个容器，或者类型（指typeclass，非面向对象编程里面的class）。 比如： 一个int类型的对象是一个简单对象，那么一个int数组则是一个更为高阶的对象。

那么，**函子表达的是范畴到范畴的映射关系**。此时，我们已经知道所谓的映射关系，就是函数而已。

举例：下面Java代码中的IM属于简单对象，Employee属于高阶对象。

{% hint style="info" %}
有时间的话，学一下Haskell这种比较纯的函数式语言会更好。然而笔者也没学过Haskell，而且在工作的时候，你所使用的语言取决于公司和团队大多数人。Java是一门使用广泛的语言，好在笔者多年从事Android(Java)开发。而又因为JavaScript不是强类型语言，Java 8的Stream API增加了对函数式编程的支持。因此，为方便举例说明一些基础概念，本文可能会临时借用Java 8来举例说明，并会用/\*Java\*/特别标明。
{% endhint %}

```java
/*Java*/

class Communication
{
    private int type;
}

//简单对象
class IM extends Communication
{
    private String code;
}

//高阶对象
class Employee
{
    private int emplNo;
    private String firstName;
    private String lastName;
    private Communication communication;
}
```

再来看看这个简单的add函数：

```javascript
/*Java*/

public int add(int someNumber)
{
    return someNumber + 3;
}
```

函数add是对简单的int对象做映射；那么对应的函子应是对int数组这个高阶对象（或者说容器）做映射，具体说来就是对int数组的每一个元素做加3的映射。所以，\*\*函子可以把一个作用于简单对象的函数，应用到一个高阶对象。\*\*如果把上面的add函数变换成对数组每一个元素加3，如下代码，Java8的`Stream`对象就是一个函子。

```javascript
/*Java*/

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
list.stream().map(i -> i + 3).forEach(System.out::println);
```

上面的代码多了个`map`方法是做什么的？没错， **函子约定会实现 map这个特定的方法（或称为函数）**，名称可能不一定是map，但是功能是一样的。

为方便理解，下面再以图例来说明如何从函数到函子的：

(1) 简单对象一般为值类型：

![](https://1680353414-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LnlixiOMXyluGpXDGGs%2F-M5imO479y8w96XsVXvJ%2F-M5iq4JRtBngPPJr_ZmW%2F6.3.1.2.1.png?alt=media\&token=a8ee20de-18d2-498e-8755-069ba40d196a)

(2) 简单对象和函数之间的关系如下图：

![](https://1680353414-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LnlixiOMXyluGpXDGGs%2F-M5imO479y8w96XsVXvJ%2F-M5iqdBUeOXBAE-V7ERp%2F6.3.1.2.2.png?alt=media\&token=b9bddfa7-b4e0-4fbd-a570-3c63dec65916)

(3) 高阶对象是一个容器(下图中的context)，包含其他对象：

![](https://1680353414-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LnlixiOMXyluGpXDGGs%2F-M5imO479y8w96XsVXvJ%2F-M5iqwSX34y9Xwbml8Y_%2F6.3.1.2.3.png?alt=media\&token=3d91790d-0ece-4527-9525-8f494ec90eb5)

(4) 对于高阶对象，不能直接应用函数，需要先打开这个容器：

![](https://1680353414-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LnlixiOMXyluGpXDGGs%2F-M5imO479y8w96XsVXvJ%2F-M5ir2WFMXXGylGzkNPV%2F6.3.1.2.4.png?alt=media\&token=11a62958-a1c5-4dab-b5a3-5423ad2cab98)

(5) 因此，定义一个新的方法 `map`来解决在高阶对象上使用函数的问题：

![](https://1680353414-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LnlixiOMXyluGpXDGGs%2F-M5imO479y8w96XsVXvJ%2F-M5irH4q5EteZ2Ll5S3w%2F6.3.1.2.5.png?alt=media\&token=47c10841-ae55-4eba-8950-91a8ff4b3007)

**这个实现了map方法的类型类（typeclass），或者说是容器，我们就称之为函子。**

{% hint style="info" %}
函数式编程中的类型类（typeclass）是定义行为的接口。 如果一个类型是某个类型类的实例，那么这个类型必须实现所有该类型类所定义的行为。不要因为有“类”这个字就把类型类与面向对象中的类混淆， 他们是完全不同的概念。类型构造器能够接收其他类型为参数，创建出新的类型。举个例子，Java的List即为接收一个类型参数的类型构造器， 当类型参数为Int时，List类型构造器的返回类型为List\<Int>，当类型参数为String时，返回类型为List\<String>。 与类型构造器相对的概念是值构造器，比如Integer(2)。
{% endhint %}

所谓的map方法，就是将高阶对象这个容器打开，取出里面的简单对象，然后再对简单对象应用map方法传递过来的函数，再把应用函数后的结果又包装成高阶对象，最后将这个高阶对象返回。

### 函子的作用

这么绕一圈儿弄出个函子的概念有什么用呢？

函子并不刻意造出来的，是先有了用法然后总结归纳出来的基础概念。

有了函子，让只能处理简单对象的函数可以处理高阶对象，而且想要什么结果，传入对应的函数给函子就行，传入不同的函数，产生不同的结果，比较灵活。换句话来说，我们让函子自己来运行这个函数，这样函子就可以自由地选择何时何地如何操作这个函数，以致于拥有惰性求值、错误处理、异步调用等等非常出色的特性。而且代码也十分简洁，在后续文章，会发现函数式编程的代码一向表现得更简洁一些。

读者可能会说，“喔，原来JavaScript里面的Array，自带了`map`方法，已经实现了函子的概念，而Array也就是函子”，的确，没错！

### 代码实现

```javascript
class Functor {
  constructor(val) { 
    this.val = val; 
  }

  map(f) {
    return new Functor(f(this.val));
  }
}
```

\*\*约定：\*\*函子是具有`map`方法的容器。该map方法将容器里面的每一个值，映射到另一个容器。

### 用法示例

```javascript
(new Functor(2)).map(function (v) {
  return v+ 1;
});
//>> Functor {val:3}

(new Functor('coffe')).map(function(s) {
  return s.toUpperCase();
});
//>> Functor {val:'COFFE'}

// _是loadash的用法，具体参见https://www.lodashjs.com/docs/lodash.curry
(new Functor('coffe')).map(_.concat(' 1891')).map(_.prop('length'));
//>> Functor {val:10}
```

上面的代码带有`new`，`new`关键字是面向对象编程的产物。函数式编程使用`of`来替换`new`生成一个新的容器。

```javascript
class Functor {
  constructor(val) { 
    this.val = val; 
  }

  map(f) {
    return new Functor(f(this.val));
  }
  
  //ES6静态方法
  static of(val) {
    return new Functor(val);
  }
}
```

那么前面的例子就可以改成：

```javascript
Functor.of(2).map(function (v) {
  return v+ 1;
});
//>> Functor {val:3}
```

是不是看起来更加函数式了？ 😃

## 08.加强版函子（Applicative）

如果理解了函子，就很容易理解加强版函子了。

函子是将简单对象放到了容器(context)里，处理这种简单对象的函数通过map方法传进来；加强版函子更进一步，把函数也放进了容器。

![](https://1680353414-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LnlixiOMXyluGpXDGGs%2F-M5kTJUNW8XblR8IzSFX%2F-M5kTboJYInI9L-zGE9T%2F6.3.1.3.1.png?alt=media\&token=b0aaf4e7-6b5c-460e-8f65-8bf2d3401d1a)

这样就可以让加强版函子A里面的函数，去处理函子B里面的值。

这么做了之后，意味着函数和函数处理的数据（简单对象或高阶对象）都被高度抽象化了。高度抽象化的结果就是高度的灵活性、可扩展性、可维护性。

### 代码实现

加强版函子一般约定会实现`ap`方法。

```javascript
class Ap extends Functor {
  ap(F) {
    return Ap.of(this.val(F.val));
  }
}
```

从上面代码可以看出，加强版函子也是函子。但是要注意，其方法ap的参数不是函数，而是另一个函子。

### 用法示例

//todo

## 09.单子（Monad）

聪明的你可能会问：“既然函子是容器，函子里面可以包含函子吗？”

“可以！函子是可以嵌套的。”

但是嵌套的函子取值很不方便。比如：

```javascript
Functor.of(Functor.of(Functor.of(1)));
```

要取出里面的值，要调用三次val：

```javascript
Functor.of(Functor.of(Functor.of(1))).val.val.val;//>> 1
```

这样不够优雅，因此就出现了单子这种类型的函子。

**单子的作用是，总是返回一个单层的函子。** 它有一个`flatMap`方法，与`map`方法作用相同，唯一的区别是如果生成了一个嵌套函子，它会取出后者内部的值，保证返回的永远是一个单层的容器，不会出现嵌套的情况。

### 代码实现

```javascript
class Monad extends Functor {
  join() {
    return this.val;
  }
  flatMap(f) {
    return this.map(f).join();
  }
}
```

上面代码中，如果函数`f`返回的是一个函子，那么`this.map(f)`就会生成一个嵌套的函子。所以，`join`方法保证了`flatMap`方法总是返回一个单层的函子。这意味着嵌套的函子会被铺平（flatten）。

细心的读者可能会发现，原来本书“壹.1.1”介绍新版ECMAScript特性时，讲到了数组的[Array.prototype.flatMap](broken-reference)，因为数组也实现了`flatMap`，因此数组也是单子。

## 10.函数式编程的好处

其实前面已经穿插的讲了很多函数式编程的好处，本篇最后再总体归纳一下。

首先，最直观的角度来说，函数式风格的代码可以写得更简洁，大大减少了程序员的键盘敲击输入量。

其次，函数式的代码是“对映射的描述”，它不仅可以描述数组这样的数据结构之间的对应关系，任何能在计算机中体现的东西之间的对应关系都可以描述——比如函数和函数之间的映射；比如外部操作到 GUI 之间的映射（就是现在前端热炒的 FRP——响应式编程的一种范式）。它的抽象程度可以很高，这就意味着函数式的代码可以更方便的复用。

再次，函数式编程将细节封装到函数内部隐藏起来，不过多的依赖外部环境，让程序员专注业务的实现，优化的时候，可以专注优化函数内部实现即可，甚至无需改动外部代码，可以让代码更容易维护。

另外，因为在函数式编程里，函数是纯函数，没有副作用，不改变函数之外的变量，不会造成死锁，因此是多线程安全的，所以函数式编程可以方便地进行并行计算。这个特性对于充分利用多核CPU尤其重要。然后，函数式编程没有副作用，只要保证接口不变，内部实现是外部无关的。所以，可以在运行状态下直接升级代码，不需要重启，也不需要停机。Erlang语言早就证明了这一点，它是瑞典爱立信公司为了管理电话系统而开发的，电话系统的升级当然是不能停机的。

还有，可以惰性求值、缓存计算以加快多次执行的速度。

最后，将代码写成这种样子可以方便用数学思维进行科学研究。

以上具体的好处，将在后续文章中逐一演示。

## 参考文献

{% hint style="info" %}
[Functors, Applicatives, And Monads In Pictures](http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html)\
[函数式编程入门教程](http://www.ruanyifeng.com/blog/2017/02/fp-tutorial.html)\
[《Java函数式编程》](https://book.douban.com/subject/27594722/)\
[《JavaScript函数式编程指南》](https://book.douban.com/subject/30283769/)
{% endhint %}
