什么是函数式编程
共 4338字,需浏览 9分钟
·
2021-03-16 19:53
作为一名开发者, 自然是想要写出优雅的, 易于维护的, 可扩展的, 可以预测的代码。函数式编程 (Functional Programming / FP) 的原则能够很好的命中这些需求。
函数式编程是一种编程范式或者说风格, 在这种范式下开发者更关注不变性, 函数是一等公民, 引用透明性, 以及纯函数性等性质. 如果你还不清楚这些名词那也不用担心, 我们将在这篇文章中逐步了解这些术语.
函数式编程从Lambda计算演变而来, Lambda计算是一种建立在函数抽象与函数推导上的数学系统. 因此, 大部分函数式编程语言看起来都十分的"数学"(译者: 比如Haskell, 实际上JS也满足函数式编程的要求). 好消息是, 并不需要通过专门使用函数式编程语言来引入函数式编程范式. 在这篇文章中, 我们将使用JavaScript来进行演示和示例. JavaScript拥有不少使它能够满足函数式编程要求的同时又不会拘泥于此的特性.
函数式编程的核心原则
既然我们已经讨论了函数式编程是什么, 现在让我们来看看函数式编程背后的核心原则。
纯函数 Pure functions
我喜欢将函数比作机器 - 它们接受一组输入(参数), 并且在之后输出一些东西(返回值). 纯函数没有"副作用"或者其他与返回值无关的行为. 一些潜在的副作用包括打印的操作, 比如console.log, 或者对函数外的变量进行操作之类的。
这是一个非纯函数的例子:
let number = 2;
function squreaNumber() {
number = number * number; // 不纯的操作: 对函数外部的变量进行了修改
consol.log(number); // 不纯的操作: 将函数内的操作打印了出来
return number;
}
相对的, 下面是一个纯函数的例子, 它接受一个输入, 并返回一个输出:
function squreNumber(number) {
return number * number;
}
squireNumber(2);
纯函数独立于函数外的状态而运行, 它们不应该依赖于任何自身内部以外的变量, 包括全局变量. 在第一个例子中, 我们使用了在函数体外部创建的变量 number
, 并且在函数体内部对它进行了修改. 这就打破了原则. 如果你深度依赖一个外部的频繁发生变动的变量, 你的代码将会变得既不可预测又难以追踪, 找出bug的位置或者解释变量的值如何变化将会变得更加困难. 相反, 使用只有输入与输出, 并且变量仅存在函数内部的函数, 将会使得调试debug的过程更为简单.
此外, 函数应该遵循引用透明性原则, 这意味着, 对于相同的输入, 函数总会输出相同的输出. 在上述的例子中, 如果对函数传入一个参数 2
, 那么它将始终返回结果 4
. 但是对于一个产生随机数的函数来说, 结果就不是这样了. 对于两次调用, 给与相同的输入, 其结果是不同的.
// 非引用透明性的
Math.random();
// 0.1406399143589343
Math.random();
// 0.26768924082159495
不可变性 Immutable
函数式编程同时也重视不可变性, 或者说不会直接修改数据. 不可变性为函数的可预测性提供支持 - 你清楚数据的值, 而且它们也不会被改变, 这将使得代码变得更加简单, 也更容易去测试, 并且也更容易在分布式和多线程应用中被调用.
当开始处理数据结构时, 不可变性会频繁地受到影响. 例如许多JavaScript中的数组方法都会直接地改变数组本身. 比如 .pop()
会直接移除数组的最后一个元素, .splice()
会将数组中的一部分移除. 而在函数式范式中, 我们会从原数组中复制一个新数组出来, 并在这个过程中移除我们想要移除的元素。
// 直接改变 myArr
const myArr = [1, 2, 3];
myArr.pop(); // 3
console.log(myArr); // [1, 2];
// 复制原数组, 并且不带上最后一个元素
const myArr = [1, 2, 3];
const myNewArr = myArr.slice(0, 2); // [1, 2]
console.log(myArr); // [1, 2, 3]
函数是一等公民 First-class functions
在函数式编程中, 函数是一等公民, 这意味着他们能够被像其他的变量那样作为值进行使用. 我们能够创建一个函数的数组, 或者将函数作为参数传递给其他函数, 或者将他们保存在变量中.
const myFunctionArr = [() => 1 + 2, () => console.log('hi'), x => 3 * x];
myFunctionArr[2](2); // 6
const myFunction = anotherFunction => anotherFunction(20);
const secondFunction = x => x * 10;
myFunction(secondFunction); // 200
阶函数 Higher-order functions
高阶函数是指完成这两个任务之一的函数: 使用一个或多个函数作为他的参数; 返回一个函数. JavaScript内建了许多第一类的高阶函数, 比如在数组中常用的 filter
, map
, reduce
.
filter
用来从原数组中, 对元素筛选满足条件的部分后保持顺序返回新的数组。
const myArr = [1,2,3,4,5];
const evens = myArr.filter(x => x % 2 === 0); // [2, 4]
map
用来遍历整个数组, 并且对每个元素根据传入的逻辑进行一个映射. 在下面这个例子中, 我们通过给 map
函数传入一个函数来将每个元素都乘以2
。
const myArr = [1, 2, 3, 4, 5];
const doubled = myArr.map(i => i * 2); // [2, 4, 6, 8, 10]
reduce
根据输入的数组输出一个单一的值, 通常用来计算数组的元素的值的总和, 或者扁平化数组, 或者将元素分组.
const myArr = [1, 2, 3, 4, 5];
const sum = myArr.reduce((i, runningSum) => i + runningSum); // 15
建议各位读者自己实现一次每个方法! 举个例子, 可以像这样创建一个 filter
函数:
const filter = (arr, condition) => {
const filteredArr = [];
for (let i = 0; i < arr.length; i++) {
if (condition(arr[i])) {
filteredArr.push(arr[i]);
}
}
return filteredArr;
}
第二类高阶函返回一个函数作为其返回值, 也是一个相对常见的范式. 举个例子:
const createGreeting = greeting => persion => `${greeting} ${person}`;
const sayHi = createGreeting('Hi');
console.log(sayHi('Ali')); // 'Hi Ali'
const sayHello = createGreeting('Hello');
console.log(sayHello('Ali')); // 'Hello Ali'
顺带一提, 函数的柯里化(Currying)是一个很类似的技术。
函数组合 Function composition
将多个简单函数按照一定顺序组合成为一个复杂函数的过程被称为函数组合. 例如可以将average与sum两个函数组合起来变成一个averageArray的函数用来Number数组的平均值. 每一个独立的function都相对较小, 并且可以被复用于其他目的, 而组合后的它们能完成更加完整而独立的任务:
const sum = arr => arr.reduce((i, runningSum) => i + runningSum);
const average = (sum, count) => sum/count;
const averageArray = arr => average(sum(arr), arr.length);
函数式编程的好处
函数式编程使得代码更加的模块化. 开发者可以使用体量更小的, 可以被一次又一次复用的函数. 了解每一个函数的功能与特性意味着能够更清晰明了地进行调试与测试. 更不用说这些函数都是可预测的.
此外, 对于多核的开发, 可以放心地向这些CPU核心分发函数的运行(译者: 因为只关心输入和输出了, 不会受到外部变量或者状态的影响), 继而能够达到更高的运行效率.
怎么样才能使用函数式编程?
开发者不需要完全地遵守每一个函数式编程的规定. 尽管面向对象编程通常被视作与函数式编程相违背的对手, 但开发者仍然可以在使用函数式编程的一些原则和特性的时候结合面向对象的编程范式来进行开发.
举例来说, React, 吸收了很多函数编程的原则, 例如不可变的state, 但同时多年来也保留了基于类的语法.
函数式编程几乎可以通过任何一个编程语言来实现, 并不需要开发者去写Clojure或者Haskell(除非你真的想).
即使函数式原则遵循得并不纯粹, 函数式编程仍然能给你的代码带来不小的好处.