React世界的函数式编程(Functional Programming)

React世界的函数式编程(Functional Programming)

学习React不可避免地要接触函数式编程(Functional Programming),今天就来说一说函数式编程,扯一扯React的世界里有多少和函数式编程相关的料。

严格说来,React并没有使用太多函数式编程(Functional Programming)的思想,但是React周边的工具(比如Redux)很多却使用了这种的思想,这可是个大好事啊!要知道这世界不只有“面向对象编程”这一种风格,不怕得罪人地说,面向对象编程实在是获得了过多的赞誉,终于该另一种编程风格上场了。

有的语言就是函数式的,比如Haskell、Scala,这些语言对函数式编程贯彻得相当彻底,兼职就是函数式编程界的原教旨主义,这里我们不去探究这些语言,因为我们这个专栏是叫《进击的React》,讲的还是React和JavaScript相关的事。

函数式编程的由来

遥想当年,盘古开天辟地,创造了计算机世界,有两位巨擘对计算机的运算能力做了模型化描述。

一位是阿兰.图灵(Alan Turing),就是奥斯卡奖电影《模仿游戏》里的图灵,计算机软件业界的祖师爷,他是一个gay,没错,我们的祖师爷是gay,所以干这行的真不应该歧视同性恋:-) 图灵提出“图灵机”的概念,大概意思就是说,假设有一个纸带和一个打孔机,然后有一套指令,能够控制打孔机在纸带上移动、能够读取当前位置是否打了孔、能够在当前位置打一个孔,这就是一个图灵机,假设一个问题能够靠这个纸带+打孔机+指令的方式解决,那就说明这个问题是“可计算的”。当然,这只是一个理论模型,实际上没人会用这种机械方式来制造计算机。

另外一个位巨擘,是邱奇(Alonzo Church)(更正:Church翻译为邱奇是业界通用译法,和当过英国首相的那个丘吉尔不同,丘吉尔是Churchill),这个邱奇是个数学家,他提出了Lambda演算(Lambda Calculus)的概念,用函数组合的方式来描述计算过程,换句话来说,如果一个问题能够用一套函数组合的算法来表达,那就说明这个问题是可计算的。


无论是“图灵机”,还是“Lambda演算”,都能够用来表达计算过程,但是这两种模型都没有被采用来生产计算机。

为什么会这样呢?

根据马克思经济学,生产力是决定性力量,计算机世界设计成这样,是因为经济问题。

按照图灵机那样建造机械方式的计算机肯定不行,而按照Lambda演算这样的方式来构建计算机也不行,因为那样造价太高。

现有的计算机体系结构都是CPU中内嵌一些寄存器(register),CPU指令就是通过这些寄存器来运算结果,寄存器数量有限,因为CPU上的寄存器很贵,从经济角度,导致一个程序要时不时要把运算结果从寄存器移到内存里面去,这一切都要由程序指令来控制,这就是命令式的编程方式(Imperative Programming),一步一步怎么做都要描述清楚。

正因为计算机诞生之初寄存器生产代价很大,所以这种很“节约”的指令方式被生产硬件的厂商采用,这种机器指令的形态决定了汇编语言的形态,汇编语言的形态决定了C语言的形态,天下武功出少林,天下语言出C语言,C语言的风格又影响了很多语言,以至于命令式编程达到了统治地位。

在计算机创世之初,并没有计算机科学家,因为没有鸡也就没有鸡蛋,是数学家、物理学家和电子工程师开辟了计算机这个新的世界,数学家的思维自上而下,首先构想的是数学模型,然后才是如何落地实现,物理学家和电子工程师的思维防止自下而上,先想的是如何实现硬件,然后才去迎合数学的描述。

很显然,自下而上的方式成为工业的选择。

不过,虽然没有直接的硬件支持,函数式编程并没有像恐龙一样绝种,比如LISP,这种语言一直存活,连Emacs编辑器(也许有人会说Emacs不只是编辑器)都是LISP语言写的。不过,函数式编程语言一直也没有特别被广为接受,因为通过一套完全不同思维方式的机器指令来模拟函数式的理想,肯定会消耗一些性能,不过,现在计算机硬件发展到足够强大的底部,我们终于可以忽略不计这些性能差异了。

总之,是时候学习函数式编程了。

React是函数式编程吗?

要回答这个问题,我们先要了解一下函数式编程是怎么一回事。

邱奇提出的Lambda演算里,有几个重要设定,在之后的函数式编程语言中实现和扩充了这些设定,这些设定很多,在这里我只列出一些主要的,依次来看React和JavaScript是否满足这些要求。

函数是第一等公民

这一点,JavaScript没有问题,在JavaScript的世界里,函数可以作为参数传递,函数可以赋值给一个变量,一个函数的执行结果也可以是一个函数,因为在JavaScript里面,函数也是对象,第一等公民的地位妥妥的。

因为函数是第一等公民,所以函数的组合就成为可能,可以玩出函数式编程才有的技巧。

数据是不可变的(Immutable)

在纯种的函数式编程语言中,数据是不可变的,或者说没有变量这个概念,所有的数据一旦产生,就不能改变其中的值,如果要改变,那就只能生成一个新的数据。

JavaScript语言本身和Immutable沾上边的只有Object.freeze这个函数,可以“冻结”一个对象,防止对这个对象的直接修改。

const obj = {name: 'Morgan'};
Object.freeze(obj);
obj.name = 'Cheng'; //strict模式下抛出异常,非strict模式下也不会修改obj.name的值。

使用一些辅助库比如immutable.js可以达到“不可变”的效果,关于immutabe.js我们回头专门讨论,在这里简单说一句:不明白就别用!

在React中,强调一个组件不能去修改传入的prop值,也是遵循Immutable的原则。

在Redux中,更是强调Immutable的作用,每个reducer不能够修改state,只能返回一个新的state。

Immutable是个好原则,可以让代码更容易维护,当你看到一个变量的时候,可以放心假设这个变量代表的值不会被篡改,否则,你就会很操心。

在JavaScript中,尽量不要使用push方法去修改一个数组。

const arr = [1, 2, 3];

arr.push(4); //这样不好,看到这代码我就方了,需要从上往下琢磨一下arr到底存的是啥
const newArr = [...arr, 4]; //这样,arr不会被修改,很放心,要修改过的版本用newArr就好了

也不要直接去修改一个对象的字段。

const me = {name: 'Morgan'};

me.skill = 'React'; //这样不好,拿不准me里是啥了
const newMe = {...me, skill: 'React'}; //这样,me不会被修改

保持Immutable的代价就会有一些空间和时间损耗,不过说真的,对绝大部分场合,真的不用去操心这点空间损耗,代码的可读性比这点损耗重要。

强制使用纯函数

所谓纯函数,就是和数学概念上的函数一样的函数,没有任何副作用,输出完全由输入决定,如果这世界上绝大部分code都是纯函数,那将是一个美好的世界,因为程序运行结果更容易预测,更不容易出bug。

在真正的函数式编程语言中,并不是写不出产生副作用的函数,毕竟程序运行没有输入输出这昂的副作用那不就没用了嘛,但是写纯函数比写不纯的函数更加方便痛快,所以还是鼓励写纯函数。

在JavaScript中,虽然我们可以通过一些手段(比如强制让this变成undefined)来避免副作用,但是JavaScript本身就是一个弱类型没有副作用检查的语言,所以没法强制使用纯函数,基本还是要靠程序员自觉。当然,我相信只要程序员充分意识到了纯函数的好处,那也不难说服他们这么写。

在React中,组件的render函数应该是一个纯函数(你要非说只需要是一个幂等函数,也行),只有这样,组件渲染的结果才只和state/props有关系,遵循UI=f(state)这个公式。

在Redux,reducer必须是一个纯函数,也是函数式编程的要求。

支持函数递归调用

JavaScript当然支持递归,不过,对于原教旨主义的函数式编程语言,根本不存在for/while这样的循环语句,需要循环执行就要靠递归。

从这一点上看来,JavaScript还不够纯粹函数式:)

我个人觉得,该用for/while还得用,这个问题上不要太原教旨主义。

函数只接受一个参数

这可能是函数式编程语言最让人难以理解的一个特性了,对于写了十几年任意参数个数函数的凡人,看到这个规矩真的要疯了,为啥一个函数只能有一个参数?一个函数只能有一个参数,岂不是好多功能都没法写了?

让函数执行返回函数,可以把多参数的函数分解为多个单参数的函数,不过,我们还是要问:为啥要这样?

答案,我们可以简单理解为——这就是规矩!当年邱奇发明Lambda演算时就定下的规矩!

当然,JavaScript语言没有限制一个函数的参数个数,所以这一点上JavaScript也不够原教旨主义,不过,知道函数式编程有这样一个规矩,可以帮助我们理解为什么Redux的一些代码写成很奇怪的样子。

比如,在Redux中,要写一个Middleware,代码是这个样子。

const someMiddleware = store => next => action => {
  // 实现middleware
};

如果看得不是很清楚,可以换个形式来看,上面的代码其实就是下面这样,一个函数返回另一个函数,利用闭包(Closure),最里面的一个函数就可以访问三个参数了。

const someMiddleware = store => {
  return next => {
    return action => {
      // 实现middleware
    };
  };
};

理论上,既然最后的函数需要的只是store、next和action三个参数,Redux完全可以定义一个middleware的形式是这样。

const someMiddleware = (store, next, action) => {
  // 实现middleware
};

实际上,Redux没有像上面那样做,就是遵照函数式编程的传统,Redux的作者Dan Abramov现在服务于React核心开发组,可以预见未来React也会更加“函数式”。

总结

JavaScript不是一个严格意义上的函数式编程语言,但是如果我们不把“函数式编程”当做一个语言特性,只是当做一种“风格”,那么JavaScript就非常适合使用这种风格,现实中存在的程序(比如React和Redux)已经证明了这一点。

React的“函数式”特征,不光体现在要求render是一个纯函数上,还存在于其他的细节之中,比如函数式的setState,在 setState为什么不会同步更新组件状态 中有详细介绍。

Redux是非常“函数式”的一个库,可以说,要理解Redux,你就需要了解函数式编程。

函数式编程是个好东西,值得去好好学习,下次细说。

文章被以下专栏收录