函数式编程入门

纯函数

什么是纯函数?

  • 纯函数:相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

为什么要追求纯函数?

  • 可缓存性(Cacheable):JavaScript 中应用最广泛的库为 memoizee
  • 可移植性(Portable):可移植性可以意味着把函数序列化。与之相对的是面向对象语言,移植一个对象通常需要将整个庞大的体系迁移,这也是 JavaScript 拥有强大的组件化生态的原因。
  • 自文档化(Self-Documenting):不需要过多的 Context 来描述函数执行前、执行后的效果;
  • 可测试性(Testable):Quickcheck,一个为函数式环境量身定制的测试工具。
  • 引用透明性(Referential Transparency):如果一个函数调用可以完全用它的返回值代替,那么称这个函数时引用透明的。

柯里化 (Curry)

Curry 的概念:

  • 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

柯里化函数的简单实现:

1
2
3
var curry = fn => function $curry(...args) {
  return (args.length < fn.length) ? $curry.bind(null, ...args) : fn.call(null, ...args);
}

函数组合 (Compose)

以下的操作称作为两个函数 fg 的组合(相当于数学中的复合函数):

1
var compose = (f,g) => ( (x) => f(g(x)) );

JavaScript 中有一个为了函数式编程设计的库就聚合了 composecurry 等函数:ramda

有一个与组合理念相关的一种设计模式,叫 Pointfree

  • Pointfree:或称隐形编程,是一种只关心函数实现而并不关心具体参数的设计模式,在实际开发过程中通常不需要把函数的参数显式地表现出来,所以代码通常看起来会更加简洁;
  • 下面是一个用 Pointfree 设计模式优化代码的例子:
1
2
3
4
5
// 通常思维的代码
var snakeCase = word => word.toLowerCase().replace(/\s+/ig, '_');

// Pointfree 方式编写的代码
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
  • 可以看到用 Pointfree 的设计模式优化之后,word 这个参数就不需要在函数定义时显式声明出来了;

PostScript

  • 小技巧:compose 函数的阅读有点类似于矩阵的乘法,如果想知道返回函数的签名,只需要知道第一个函数的参数和最后一个函数的返回值即可。

范畴论 (Category Theory)

数学上有一个分支‘范畴论“通过研究一系列抽象的概念形式化地统一了集合论、类型论、群论等不同的分支。

范畴论中定义了 category,它被定义为以下组件的抽象集合:

  1. A collection of objects:对象的集合。比如 Boolean 类型可以理解为 true/false 的集合;
  2. A collection of morphism:映射的集合。比如我们在上面提到的纯函数,就是我们主要研究的映射;
  3. A notion of composition on the morphism:映射的组合。就是我们在前面提到的 compose 操作;
  4. A distinguished morphism called identity:一个特殊的映射,恒等映射。在组合关系中恒等映射即为函数 id = x => x

Hindley-Milner 类型系统

什么是类型:

  • 函数签名在这个类型系统中可以被认为是“类型到类型的映射”。
  • 基于这个逻辑,“类型”在“类型系统”中可以被认为是一个变量。TypeScript 基于这个逻辑通过“泛型”设计了一个图灵完全系统。

Parametricity:

  • Parametricity 是一种参数多态化函数,统一都满足的抽象性质。它指明了无论多态化参数实例化为那种真实函数,这些函数都有同样的表现形式;
  • 比如说有这样的函数签名 [a] => a,因为 a 是任意的类型,所以这个函数只能在明确的类型 Array 上进行一些操作(比如取它的第一个、最后一个、或随机的一个元素)。

Free Theorem(自由定理,见论文):

  • 因为参数多态化函数有上面的 Parametricity 性质,可以推导出函数许多相关的性质。
  • 比如下面就是自由定理推导出的一些结果:
1
2
3
compose(f, head) === compose(head, map(f));

compose(map(f), filter(compose(p ,f))) === compose(filter(p), map(f));
  • 这些看起来纯理论毫无价值的公式,实际上是有应用价值的。比如说上面的第一个公式理论上证明它们的计算结果是一样的,但是后者的计算量却比前者要大很多。

Constraints:

  • 类型系统可以声明类型映射的参数满足一定的约束。这一理论化的内容在 TypeScript 中是通过 extends 关键字实现的。