函数式编程

函数式编程

函数式编程(通常简称为FP)是指通过符合纯函数来构建软件的过程。它避免了共享状态(share state)易变的数据(mutable data)以及副作用(side-effects)。函数式编程是一种编程范式,一种软件构建的思维方式。

以下这些名词定义中蕴含了许多思想,只有理解了它们,才能够开始掌握函数式编程真正的意义:

  • 纯函数(Pure functions)
  • 函数复合(Function composition)
  • 避免共享状态(Avoid shared state)
  • 避免改变状态(Avoid mutating state)
  • 避免副作用(Avoid side effects)

1.纯函数

  • 给它同样的输入,总是返回同样的结果;
  • 没有副作用;

纯函数有着许多对函数式编程而言非常重要的属性,包括引用透明(你可以将一个函数调用替换成它的结果值,而不会对程序的运行造成影响。详细:什么是纯函数

2.函数复合

  • 函数复合是结合两个或多个函数,从而产生一个新函数或进行某些计算的过程。eg:f(g(x))

详细:函数复合

3.共享状态

  • 共享状态 的意思是任意变量、对象或者内存空间存在于共享作用域下,或者作为对象的属性在各个作用域之间被传递。共享作用域包括全局作用域和闭包作用域。

同步竞争和调用时序变更导致的问题都是共享状态常见的bug。

  • 同步竞争:举个例子,操作A向服务器请求改变B的状态,然后马上用C操作向服务器发送请求,该请求也会改变B的状态,不幸的是C操作有可能早于A返回,这就导致了同步竞争的bug。
  • 调用时序变更:这个好理解,因为共享状态操作是有时序的,如果颠倒执行顺序就会导致共享状态出现错乱。

4.不可变性

一个不可变的(immutable)对象是指一个对象不会在它创建之后被改变。不可变性是函数式编程的一个核心概念,因为没有它,你的程序中的数据流是有损的。

JavaScript 提供了一个方法,能够浅冻结一个对象:

1
2
3
4
5
6
7
8
9
const a = Object.freeze({
foo: 'Hello',
bar: 'world',
baz: '!'
});
a.foo = 'Goodbye';
// Error: Cannot assign to read only property 'foo' of object Object

但是深层仍旧可以被改变。

在许多函数式编程语言中,有特殊的不可变数据结构,被称为 trie 数据结构(trie 的发音为 tree),这一结构有效地深冻结 —— 意味任何属性无论它的对象层级如何都不能被改变。

有一些 JavaScript 的库使用了 tries,包括 Immutable.jsMori

immutable更多使用实例:10 Tips for Better Redux Architecture

ps: 在 JavaScript 中,很重要的一点是不要混淆了 const 和不变性。const 创建一个变量绑定,让该变量不能再次被赋值。const 并不创建不可变对象。你虽然不能改变绑定到这个变量名上的对象,但你仍然可以改变它的属性,这意味着 const 的变量仍然是可变的,而不是不可变的。

5.副作用

副作用是指除了函数返回值以外,任何在函数调用之外观察到的应用程序状态改变。副作用包括:

  • 改变了任何外部变量或对象属性(例如,全局变量,或者一个在父级函数作用域链上的变量)
  • 写日志
  • 在屏幕输出
  • 写文件
  • 发网络请求
  • 触发任何外部进程
  • 调用另一个有副作用的函数

6.使用高阶函数提升重用性

高阶函数指的是一个函数以函数为参数,或以函数为返回值,或者既以函数为参数又以函数为返回值。高阶函数经常用于:

  • 抽象或隔离行为、作用,异步控制流程作为回调函数,promises,monads,等等……
  • 创建可以泛用于各种数据类型的功能
  • 部分应用于函数参数(偏函数应用)或创建一个柯里化的函数,用于复用或函数复合。
  • 接受一个函数列表并返回一些由这个列表中的函数组成的复合函数。

函数式编程倾向于复用一组通用的函数功能来处理数据。面向对象编程倾向于把方法和数据集中到对象上。那些被集中的方法只能用来操作设计好的数据类型,通常是那些包含在特定对象实例上的数据。

在函数式编程里,对任何类型的数据一视同仁。同样的 map() 操作可以 map 对象、字符串、数字或任何别的类型,因为它接受一个函数参数,来适当地操作给定类型。函数式编程通过使用高阶函数来实现这一技巧。

关于高阶函数就不展开了。自行google。

7.命令式 vs 声明式

函数式编程是声明式的,而不是命令式的,应用程序的状态通过纯函数流转。

  • 声明式:意思是说程序逻辑不需要通过明确描述控制流程来表达。程序抽象了控制流过程,花费大量代码描述的是数据流:即做什么。
  • 命令式:程序花费大量代码来描述用来达成期望结果的特定步骤 —— 控制流:即如何做。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 命令式
const doubleMap = numbers => {
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
return doubled;
};
console.log(doubleMap([2, 3, 4])); // [4, 6, 8]
// 声明式
const doubleMap = numbers => numbers.map(n => n * 2);
console.log(doubleMap([2, 3, 4])); // [4, 6, 8]
// 命令式 代码中频繁使用语句。语句是指一小段代码,它用来完成某个行为。
// 声明式 代码更多依赖表达式。表达式是指一小段代码,它用来计算某个值。

8.结论

函数式编程偏好:

  • 使用纯函数而不是使用共享状态和副作用
  • 让可变数据成为不可变的
  • 用函数复合替代命令控制流
  • 使用高阶函数来操作许多数据类型,创建通用、可复用功能取代只是操作集中的数据的方法。
  • 使用声明式而不是命令式代码(关注做什么,而不是如何做)
  • 使用表达式替代语句