reduceRight() 列表操作也称为the "right fold"。它的基本递归定义类似于以下伪代码:
function reduceRight(func, seq, start) {
if (isEmpty(seq)) {
return start;
} else {
let [first, rest] = getFirstAndRest(seq);
return func( first, reduceRight(func, rest, start) );
}
}
注意func 回调有两个参数;通常 first 参数是序列中的值,而 second 参数是累加器。这样,当您扩展 reduceRight() 在重复调用 func 方面所做的事情时,序列中较早的值将位于左侧,而较晚的值将位于右侧。从这里你有你的倒退,但我从这里开始用传统的方式。
了解如何没有理由尝试显式“反转”序列来实现这一点。因为非空序列上的reduceRight 是根据序列其余部分的reduceRight 编写的,所以这自然会关联到右侧,因此在处理之前的列表元素之前处理后面的列表元素:reduceRight(f, sequenceOf(a, b, c), z) 将评估为f(a, f(b, f(c, z))) .
这里至关重要:你对LazySequence<T>的定义:
interface LazySequence<T> {
value: T;
next(): LazySequence<T>;
}
代表一个真正的无限序列。根据这个定义,LazySequence<T> 肯定有一个next() 方法,它肯定会返回一个LazySequence<T>。 (这假设您使用的是the --strictNullChecks compiler option,您应该使用它。)例如:
function iterate<T>(init: T, func: (v: T) => T): LazySequence<T> {
return { value: init, next: () => iterate(func(init), func) }
}
const naturalNumbers = iterate(0, x => x + 1);
这里naturalNumbers对应无限序列[0, 1, 2, 3, ...]。如果我想在10 之后停止,则无法直接执行此操作。您可以定义一个名为 nil 的元素,其 value 是 undefined 并且其 next() 指向自身,因此您的序列只有有限数量的 distinct 元素,但它仍然是无止境的:
const nil: LazySequence<undefined> = { value: undefined, next: () => nil };
const fromArray = <T,>(x: T[], i = 0): LazySequence<T | undefined> =>
i < x.length ? { value: x[i], next: () => fromArray(x, i + 1) } : nil;
const someNumbersThenUndefineds = fromArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
这里someNumbersThenUndefineds对应序列[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, undefined, undefined, undefined, undefined, ...]。
大问题:由于像reduceRight() 这样的折叠通常会将整个序列缩减为单个值,因此编写一个能够胜任的算法版本是很棘手的。 t 只是陷入无限循环(或者,更可能是函数式编程,溢出堆栈)。如果您需要实际读取无限序列的每个元素,那您的日子会很糟糕。
一种方法是重新定义LazySequence<T>,使其可能是undefined:
type LazySequence<T> = {
value: T,
next(): LazySequence<T>;
} | undefined;
现在您可以编写一个有限序列,方法是让最后一个元素在您调用 next() 时返回 undefined:
const fromArray = <T,>(x: T[], i = 0): LazySequence<T> =>
i < x.length ? { value: x[i], next: () => fromArray(x, i + 1) } : undefined
const someNumbers = fromArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
现在someNumbers 更准确地表示[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]。有了这个,我们终于可以写reduceRight():
function reduceRight<T, V>(
func: (value: T, accumulator: V) => V,
seq: LazySequence<T>,
start: V
): V {
return seq ? func(seq.value, reduceRight(func, seq.next(), start)) : start;
}
我们可以看到它的实际效果:
const sumOfSomeNumbers = reduceRight((v, a) => v + a, someNumbers, 0);
console.log(sumOfSomeNumbers) // 55
另一种方法是保持LazySequence<T> 的定义不变,但如果value 是undefined,则使reduceRight() 退出:
function reduceRight<T, V>(
func: (value: T, accumulator: V) => V,
seq: LazySequence<T | undefined>,
start: V
): V {
return seq.value === undefined ? start : func(seq.value, reduceRight(func, seq.next(), start))
}
这不像第一个版本那样完全通用,因为它要求您在LazySequence<T | undefined> 上操作,其中值可以是undefined,但它的工作方式类似。如果我们将其应用于上面的someNumbersThenUndefineds,我们会得到相同的结果:
const sumOfSomeNumbers = reduceRight((v, a) => v + a, someNumbersThenUndefineds, 0);
console.log(sumOfSomeNumbers) // 55
最后,如果我们看到实际上是无限的列表,我们该怎么办?对于上述每个实现,答案都是:堆栈溢出或“递归过多”。但你不必这样做。有一种写reduceRight()的方法,这样如果func(value, accumulator)回调不需要咨询value,那么它可以提前返回。在像 Haskell 这样原生支持 lazy evaluation 的语言中,这是免费的。
如果 JavaScript 以这种方式工作,您可以编写 const f = (x, y) => y 并调用 f(somethingThatMightBlowUp(), 1),它会返回 1,甚至无需评估 somethingThatMightBlowUp。因为我们不必使用thunk 来模拟这个。我们不要求将V 类型的accumulator 直接传递给func(),而是接受() => V 类型的accThunk。如果我们不调用accThunk(),那么我们可以终止递归。
这是一个真正的懒惰 reduceRight(),它期望无限的 LazySequence<T> 不一定有 undefined 元素:
function reduceRight<T, V>(
func: (val: T, accThunk: () => V) => V,
seq: LazySequence<T>
): V {
return func(seq.value, () => reduceRight(func, seq.next()));
}
请注意,没有start 参数。列表是无限的;我们永远不需要它。相反,您会发现回调函数func 实现将需要决定是否调用accThunk(),如果没有,它将返回类似start 的内容。所以新的func 就像旧的func 和start 在一起。
让我们将naturalNumbers中小于或等于10的元素加在一起:
const sumOfNaturalNumbersAtMostTen = reduceRight(
(value, accThunk: (() => number)) => value > 10 ? 0 : accThunk() + value,
naturalNumbers
);
console.log(sumOfNaturalNumbersAtMostTen); // 55
万岁!
Playground link to code