函数式编程简介
这篇文章源自我在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;
}
一等函数
“一等公民”这个术语可能看起来很奇怪,但它的意思是函数可以像其他数据类型一样被传递和使用。例如,字符串、整数、浮点数等等。支持一等公民函数的编程语言允许我们将函数传递给其他函数。可以将其想象成依赖注入。如果你使用过 JavaScript,就会知道一等公民函数无处不在,我们将在接下来的示例中更详细地介绍它们。
例子
// robot expects a function to be passed in
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
并将其添加到我们的和中。现在我们得到了0 + 1 = 1
。然后我们重复此步骤,取出2
并添加到和中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);
}
在这个函数中,我们传入要循环的列表和一个索引,用于确定当前在列表中的位置。如果到达列表末尾,或者传入了一个无效的列表,则循环结束。如果没有到达,则loop
再次调用,并增加索引。尝试在console.log(list[index])
循环函数内部,在 之前添加一个return loop(list, index + 1);
!我们应该会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
参数(accumulation),它将跟踪列表中给定位置的总和。我们还直接使用函数获取与列表中当前项相加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
从数组开始,会怎么样[]
?