学习折叠 JS 数组
什么是折叠?
创建折叠
你可能遇到过需要获取一个数组并“收集”它们的情况。我的意思是,对数组执行一些操作,以便最终只获取单个值。以下是一些示例。
您之前肯定必须对数字数组进行求和:
function sum(numbers) {
let acc = 0;
for (const num of numbers) {
acc = add(acc, num);
}
return acc;
}
或者获取数字数组的乘积:
function prod(numbers) {
let acc = 1;
for (const num of numbers) {
acc = mult(acc, num);
}
return acc;
}
或者在数字数组中找到最大的数字:
function maximum(numbers) {
let acc = -Infinity;
for (const num of numbers) {
acc = max(acc, num);
}
return acc;
}
在每个例子中,我们都获取了一系列事物并执行了一些操作,将这些事物收集成一个事物。
什么是折叠?
上述示例有一些共同点。它们都涉及一些非常相似的部分:
- 保存最终结果的地方,通常称为累积或
acc
- 累积的初始值(0、1 和
-Infinity
) - 将累加和我们当前正在处理的数组项结合起来的二元运算(
add
,mult
和max
)
这个收集物品的过程显然遵循一种模式。我们目前重复了很多代码,所以如果我们能将它们抽象成一个函数,我们的代码就会更加简洁,表达能力也更强。这种函数有个名字叫 Fold(维基百科)。这个函数是函数式编程的基础之一。我们要做的是自己用 JS 实现这个 Fold,何乐而不为呢?
一些观察
关于折叠,有三件事值得注意。
二元运算add
、mult
和max
被称为reducers
。 Reducer 接受两个值——当前累加值和当前数组元素——并返回新的累加值。
初始值需要是identity
相对于 Reducer 的 。这意味着当初始值与另一个值 一起传递给 Reducer 时x
,输出始终为x
。示例:add(0, x) = x
mult(1, x) = x
max(-Infinity, x) = x
。
这里,0
、1
和-Infinity
分别相对于 Reducer add
、mult
和max
是恒等式。我们需要它是 ,identity
因为我们希望初始累加为“空”。0
相对于求和为空,1
相对于乘积为空。
所有数组元素必须属于同一数据类型(例如 type A
),但累加元素的数据类型(例如B
)不必与数组元素的数据类型相同。例如,此代码将一个数字数组折叠成一个字符串。
注意减速器的接口必须是reducer(acc: B, x: A): B
,在本例中是
concatNum(acc: string, x: number): string
创建折叠
说了这么多,我们终于可以开始写 fold 函数了。fold 是一个高阶函数(我强烈推荐用Eloquent JavaScript来介绍 HOF),它接受一个 Reducer(一个函数)、一个用于累加的初始值和一个数组(更正式的说法是列表,也就是JS 数组)。
我们首先泛化 add/mult/max 的 reducer,并命名为reducer
(惊喜!)。我们将初始值称为init
。然后我们泛化数组。它可以是任何内容的数组,而不仅仅是数字,所以我们将其命名为xs
。现在,我们定义了 fold!
const fold = (reducer, init, xs) => {
let acc = init;
for (const x of xs) {
acc = reducer(acc, x);
}
return acc;
};
你注意到参数传入折叠的顺序了吗?我们先传入reducer
,然后传入init
,最后传入是有原因的。这和柯里化xs
有关,我们以后会再讲。上面的例子现在看起来像这样,粗箭头样式:
const sum = xs => fold(add, 0, xs);
const prod = xs => fold(mult, 1, xs);
const maximum = xs => fold(max, -Infinity, xs);
好多了。
如果需要,我们可以内联编写 reducer:
const sum = xs => fold((acc, x) => acc + x, 0, xs);
const prod = xs => fold((acc, x) => acc * x, 1, xs);
const maximum = xs => fold((acc, x) => (acc >= x) ? acc : x, -Infinity, xs);
这是一个供您使用的交互式编辑器:
很简单,对吧?好吧,我们有点作弊了。我们在 fold 定义中使用了 for 循环(更确切地说是 for...of 循环),这在函数式编程的世界里是大忌。使用 for 循环进行数据转换意味着我们必须修改一些对象。在这里,我们acc
通过在循环中重新赋值来实现修改。真正的 fold 函数式实现应该使用递归,这样可以避免修改。我们将在另一篇文章中探讨这个问题。
给感兴趣的人一些提示
- JS 已经有一个 fold 方法,它可用于数组。它叫做reduce。所以你可能会说我们自己重新实现 fold 是毫无意义的 🤷♂️(尽管我希望它能对一些 FP 新手有所帮助)。
- 因为我们使用了 for...of 循环而不是普通的 for 循环,所以我们所做的折叠不仅适用于数组,还适用于任何可迭代对象。
- 一般来说,折叠应该适用于任何可枚举数据源,例如列表和树。
- “收集”的概念不一定非要像加法或乘法那样组合数组元素。它可以是“查找和替换”,例如最大/最小 Reducer,也可以是“顺序应用”,例如将函数应用 Reducer 连接到管道函数(如果你感兴趣的话)。应用无穷无尽!
一个函数接受一堆参数却只返回一个值,这看起来可能有点琐碎,但我们会在下一篇文章中通过实现多个折叠来见证它到底有多强大。我们将扁平化数组、管道函数,并且(希望)用折叠实现更多功能。
鏂囩珷鏉ユ簮锛�https://dev.to/mebble/learn-to-fold-your-js-arrays-2o8p