【问题标题】:How to implement reduceRight function in Typescript in Lazy Evaluation Paradigm如何在惰性评估范式中的 Typescript 中实现 reduceRight 函数
【发布时间】:2021-10-22 16:48:08
【问题描述】:

我需要帮助实现 reduceRight 函数,我对基本的 reduceRight 感到困惑我只是将一个列表反转并在其上调用 reduce 函数并获得所需的输出,但我不知道如何在 Lazy Eval 范例中做同样的事情。以下是我的 reduceRigth 代码

interface LazySequence<T> {
   value: T;
   next(): LazySequence<T>;
}

 ----------------------------------------------------------------

 function reduceRight<T>(func: (v:T, t: T)=>T, seq: LazySequence<T>, start:T): T{
    while (seq.next() === undefined){
    return reduceRight(func,seq,func(start,seq.value));
    }
    reduceRight(func,seq.next(),func(start,seq.value));
 }

【问题讨论】:

  • 您没有使用--strictNullChecks--strict 编译器选项吗?如果您要检查seq.next() == undefined,您真的应该将next() 的返回类型定义为LazySequence&lt;T&gt; | undefinedLazySequence&lt;T&gt; | undefined | null。你能做出这样的改变吗?
  • @T.J.Crowder 您可以在不访问所有值的情况下懒惰地执行reduceRight(),但是您需要回调来给您提供的不是(accumulator: V, value: T) 参数,而是(accumulatorThunk: () =&gt; V, value: T) 之类的参数。然后在回调 impl 中,您可以在决定调用 accumulatorThunk() 之前先检查 value。请参阅this demo implementation 了解它的工作原理。这在像 Haskell 这样的惰性优先语言中更容易,其中像 f(x) 这样的函数调用不会评估 x,直到 f 的主体需要它;要在 JS 中得到它,你需要一个 thunk。
  • 我喜欢函数式编程和懒惰的评估,但是天哪,@Sandy 已经发布并删除了三个关于此的问题,所以我不热衷于写一个大答案,只是因为它在我尝试时被拒绝提交。 ????
  • @jcalz - 啊,如果回调支持惰性 eval 并且知道它不需要基于累加器值的值,那就足够公平了。不是在寻找整个董事会。 :-)
  • @Sandy 如果您不想更改LazySequence&lt;T&gt; 的定义,那么您可能应该让reduceRight 接受LazySequence&lt;T | undefined&gt; 之类的参数,然后检查value正在undefined 停止。喜欢this。如果您对此感到满意,或者对更改 LazySequence 的定义或确实没有删除问题的任何内容感到满意,请告诉我,我很乐意写下答案。

标签: typescript functional-programming lazy-evaluation


【解决方案1】:

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&lt;T&gt;的定义:

interface LazySequence<T> {
   value: T;
   next(): LazySequence<T>;
}

代表一个真正的无限序列。根据这个定义,LazySequence&lt;T&gt; 肯定有一个next() 方法,它肯定会返回一个LazySequence&lt;T&gt;。 (这假设您使用的是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 的元素,其 valueundefined 并且其 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&lt;T&gt;,使其可能是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&lt;T&gt; 的定义不变,但如果valueundefined,则使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&lt;T | undefined&gt; 上操作,其中值可以是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) =&gt; y 并调用 f(somethingThatMightBlowUp(), 1),它会返回 1,甚至无需评估 somethingThatMightBlowUp。因为我们不必使用thunk模拟这个。我们不要求将V 类型的accumulator 直接传递给func(),而是接受() =&gt; V 类型的accThunk。如果我们不调用accThunk(),那么我们可以终止递归。

这是一个真正的懒惰 reduceRight(),它期望无限的 LazySequence&lt;T&gt; 不一定有 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 就像旧的funcstart 在一起。

让我们将naturalNumbers中小于或等于10的元素加在一起:

const sumOfNaturalNumbersAtMostTen = reduceRight(
  (value, accThunk: (() => number)) => value > 10 ? 0 : accThunk() + value,
  naturalNumbers
);

console.log(sumOfNaturalNumbersAtMostTen); // 55

万岁!


Playground link to code

【讨论】:

    猜你喜欢
    • 2019-11-16
    • 1970-01-01
    • 2015-01-17
    • 2014-02-08
    • 1970-01-01
    • 2015-05-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多