这篇文章来源于一个我参加Boise Code Camp 2019时的一个比较命令式编程和函数式编程解决问题的例子。目的不是教授完整的函数式编程,而是介绍一种不同于传统方法(循环、转换等)的思维方式。当遇到问题时,拥有不同的参考框架作为工具箱添加更多的工具。
函数式编程的基础可以分为三个主要概念:
- 不可变数据结构
- 纯函数
- 头等函数
让我们快速了解一下这些要点的含义。
不可变数据结构
当和像JavaScript的编程语言工作时,我们可以将数据分配给变量let myVariable = 5;
。但是,没有什么能阻止稍后在myVariable = "Now I'm a string."
。这可能很危险——也许另一个函数依赖于myVariable
作为一个数字,或者如果一些异步函数同时依赖于myVariable
工作!我们可能会遇到合并冲突。
示例:
const obj = {
a: 1,
b: 2
};
function addOne(input) {
return {
a: input.a + 1,
b: input.b + 1
};
}
const newObj = addOne(obj);
newObj === obj; // false
纯函数
纯函数是无副作用的。这意味着什么?好吧,一个只根据输入来计算输出的函数被当做一个纯函数。如果我们的函数接受一个输入,执行数据库更新,然后返回一个值,那么我们的代码中包含一个副作用——更新数据库。多次调用函数可能不会经常返回相同的结果(内存溢出、数据库被锁等)。拥有纯函数对于帮助我们编写无缺陷、易于测试的代码至关重要。
示例:
// 非纯函数相加
function notPureAdd(a, b) {
return a + new Date().getMilliseconds();
}
// 纯函数相加
function pureAdd(a, b) {
return a + b;
}
头等函数
术语"头等函数"可能看起来很奇怪,但是它全部的含义是可以像使用其他数据类型一样传递和使用函数。比如:string,int,float等。支持"头等函数"的编程语言让我们向其他函数传递函数。把它想象成依赖注入。如果你已经在工作的任何地方使用JavaScript的头等函数,那我们将接下来的例子中接触更多。
示例:
// robot 想要传入一个 function
function robot(voiceBox) {
return voiceBox("bzzzz");
}
// console.log is a function that logs to the console
robot(console.log);
// alert is a function that shows a dialog box
robot(alert);
比较命令式编程和函数式编程
为了展示命令式编程和函数式编程的基本比较,让我们对一个数组[1, 2, 3, 4]
中的数字加到一起,获得它们的合。
我们可以命令式地像下面这样写一些:
const list = [1, 2, 3, 4];
let sum = 0;
for (let i = 0; i < list.length; i++) {
sum += list[i];
}
console.log(sum); // 10
将这个转换成一个函数的方式,我们有一个大问题。在列表的每次迭代中,我们都会改变sum
的值。记住。。。不可变数据结构。
为了让这个代码正常工作,我们来分解求和的计算方式。
首先,我们从某个值开始,在我们的例子中是0
(见let sum = 0;
这一行)!接下来,我们拉出数组中的第一项1
,并将其添加到我们的sum
中。现在,我们有0 + 1 = 1
。然后我们重复这一步,拉出2
并加到sum
中1 + 2 = 3
。继续这样直到我们遍历数组的长度。
以不同的方式将内容可视化:
0 + [1,2,3,4]
0 + 1 + [2,3,4]
1 + 2 + [3,4]
3 + 3 + [4]
6 + 4
10
我们可以将这个算法看作两个独立的函数,首先我们需要某种方法将数字相加。
function add(a, b) {
return a + b;
}
简单!
接下来,我们需要一个方法来循环遍历给定的数组。由于大多数函数式编程通常依赖于递归而不是循环,因此我们将创建一个递归函数,该函数将循环遍历数组。让我们看看可能是什么样子。
function loop(list, index = 0) {
if (!list || index > list.length - 1) {
// We're at the end of the list
return;
}
return loop(list, index + 1);
}
在这个函数中,我们获取要循环遍历的列表,以及一个索引,我们将使用它来确定当前列表中的位置。如果我们到达了列表的末尾,或者给出了一个无效的列表,那么我们就完成了循环。如果没有,我们将再次调用循环,添加索引。尝试在循环函数内部return loop(list, index + 1)
之前添加一个console.log(list[index])
。我们应该看到1 2 3 4
打印到控制台上!
最后为了计算我们的数组,我们将需要合并loop
和add
函数。在阅读本例时,请记住上面的算法:
function loop(list, accu = 0, index = 0) {
if (!list || index > list.length - 1) {
return accu;
}
const result = add(accu, list[index]);
return loop(list, result, index + 1);
}
我们在loop
函数中重新排列了一些参数。现在我们有一个accu
参数(积累),它将追踪列表中给定位置的合。我们也直接用add
函数来获取添加到当前项accu
的结果。如果我们console.log(loop(list));
,我们应该将结果10
打印到控制台!
我们再往前走一步怎么样?如果我们不想把数字列表求和,而是把它们相乘呢?目前,我们必须复制loop
函数,黏贴它,然后将add
函数更改为其他东西(可能multiply
(相乘)?)。多么痛苦啊!还记得头等函数吗?我们可以在这里使用这个想法来试代码更通用。
function loop(func, list, accu = 0, index = 0) {
if (!list || index > list.length - 1) {
return accu;
}
const result = func(accu, list[index]);
return loop(func, list, result, index + 1);
}
在上面的例子中,唯一改变的东西是我们现在给loop
添加了一个函数当做新的参数。我们将调用传入的函数来获取结果,而不是add
。现在我们可以很简单地对列表进行add
、multiply
、subtract
等。
loop(add, list)
loop(function(a, b) { return a * b; }, list)
我们再也不只是循环遍历数组,而是像折叠纸一样折叠数组,直到得到一个结果。在函数式编程中,这种函数可以被称为fold
,且在JavaScript中,我们将它看作reduce
!
function reduce(func, list, accu = 0, index = 0) {
if (!list || index > list.length - 1) {
return accu;
}
const result = func(accu, list[index]);
return reduce(func, list, result, index + 1);
}
结尾
我们研究了函数式编程的基础知识,以及如何分解一个问题,从而为同一个问题提供不同的解决方案。reduce
被视为其它操作的基础,比如map()
或filter()
。这是我为你做的测试,我们如何只使用刚才创建的reduce()
来实现这两个函数?
提示
记得reduce
的算法吗?
0 + [1,2,3,4]
0 + 1 + [2,3,4]
1 + 2 + [3,4]
3 + 3 + [4]
6 + 4
10
如果不是从0
开始,而是从一个[]
数组开始呢?