简体   繁体   English

这个递归 function 是否存在设计问题?

[英]Is there a design issue in this recursive function?

 function range(x, y) { const arr = [] const innerFunc = (x, y) => { if (x === y - 1) { return arr } arr.push(x + 1) return innerFunc(x + 1, y) } innerFunc(x, y) return arr } console.log(range(2, 9))

output: [ 3, 4, 5, 6, 7, 8 ] It is a function with a recursive function inside. output: [ 3, 4, 5, 6, 7, 8 ] 它是一个 function 内部有一个递归 function。 I did this because every time I recursively called this function, the array will reset.我这样做是因为每次我递归调用这个 function 时,数组都会重置。

As far as I know only Safari in strict mode has implemented tail call optimisation (TCO) which is (as I understand it) a way to optimise recursive functions when the recursive call occurs at the end (ie the tail) eg,据我所知,只有 Safari 在严格模式下实现了尾调用优化(TCO),这是(据我了解)当递归调用发生在末尾(即尾部)时优化递归函数的一种方法,例如,

function range(x, y) {
  // ...
  return range(x+1, y);
  //     ^.................recursive call at the end
}

Without TCO recursive functions can fail and yours is no exception.如果没有 TCO,递归函数可能会失败,您的也不例外。 I worked out that after approx.我在大约之后解决了这个问题。 ±9500 calls your function will exhaust the stack: ±9500 调用您的 function 将耗尽堆栈:

range(1, 9500);
// Uncaught RangeError: Maximum call stack size exceeded

Thinking recursively递归思考

As Nick Parsons rightly pointed out you want to apply your recursive function to a shrinking set of input until your reach your exit condition.正如尼克帕森斯正确指出的那样,您希望将递归 function 应用于一组缩小的输入,直到达到退出条件。

Assuming the easiest use case where x < y this is what I'd do:假设x < y这就是我要做的最简单的用例:

const range =
  (x, y, ret = []) =>
    (x + 1 === y
      ? ret
      : range(x + 1, y, (ret.push(x + 1), ret)));

range(2, 9);
//=> [3, 4, 5, 6, 7, 8]

However this function will also exhaust the stack when it recurses too much.然而,这个 function 在递归过多时也会耗尽堆栈。 In fact it's even worse because I worked out that it will at approx.事实上,它甚至更糟,因为我计算出它会在大约。 ±7000 calls: ±7000 次调用:

range(1, 7000);
// Uncaught RangeError: Maximum call stack size exceeded

So arguably you already had done a very good job and this is no improvement at all.所以可以说你已经做得很好了,这根本没有改善。

Can we fix this?我们能解决这个问题吗?

Turns out we can with a technique known as "trampoline" but first let's see what is the problem we're trying to fix.事实证明,我们可以使用一种称为“蹦床”的技术,但首先让我们看看我们要解决的问题是什么。

When you do range(1, 10) your computer does something like this:当您执行range(1, 10)时,您的计算机会执行以下操作:

range(1, 10)
  range(2, 10)
    range(3, 10)
      range(4, 10)
        range(5, 10)
          range(6, 10)
            range(7, 10)
              range(8, 10)
                range(9, 10)
                  range(10, 10)
<-- return

Where each call is held into memory until your reach your exit condition and return the accumulation to the user.每个调用都保留在 memory 中,直到您达到退出条件并将累积返回给用户。 So we can sort of imagine that with lots of calls, if the computer doesn't protect itself, you will eventually exhaust all memory and cause crashes.所以我们可以想象,如果有很多调用,如果计算机不保护自己,你最终会耗尽所有 memory 并导致崩溃。

With a trampoline it would look something like this:使用蹦床,它看起来像这样:

range(1, 10)
  range(2, 10)
range(3, 10)
  range(4, 10)
range(5, 10)
  range(6, 10)
range(7, 10)
  range(8, 10)
range(9, 10)
  range(10, 10)
<-- return

The idea behind "trampolining" a function is to have the recursive function return a function to compute the next call to itself and have those functions executed in a loop. “蹦床” function 背后的想法是让递归 function 返回 function 以计算对自身的下一次调用并在循环中执行这些函数。

Here's a contrived example:这是一个人为的例子:

const range = (x, y, z) => {
  if (x + 1 === y) return z;
  return () => range(x + 1, y, (z.push(x + 1), z));
};

const trampoline = fn => (x, y, z = []) => {
  let res = fn(x, y, z);
  while (typeof res == 'function') res = res();
  return res;
};

trampoline(range)(2, 9);
//=> [3, 4, 5, 6, 7, 8]

trampoline(range)(1, 20000); // <-- a classic recursive function would have exploded here
//=> [2, …, 19999]

Above range computes an Array and uses Function to signal there is more work to be done.上述range计算一个数组并使用 Function 表示还有更多工作要做。 But what if we wanted to compute a function?但是如果我们想计算 function 怎么办? Recursion is a functional style, after all.毕竟,递归是一种函数式风格。 Another pitfall above is trampoline makes an assumption about the input function's parameters.上面的另一个缺陷是trampoline对输入函数的参数进行了假设。 Trampolines can be implemented in countless ways and below we address some of those concerns:蹦床可以通过无数种方式实现,下面我们将解决其中一些问题:

 const range = (x, y, z = []) => { if (x + 1 === y) return z; return call(range, x + 1, y, (z.push(x + 1), z)); // <- call }; const call = (f, ...args) => // <- call is a simple object ({ call, f, args }) const trampoline = fn => (...init) => { let res = fn(...init); while (res?.call) // <- check for call res = res.f(...res.args); // <- apply call return res; }; console.log(JSON.stringify(trampoline(range)(2, 9))); //=> [3, 4, 5, 6, 7, 8] console.log(JSON.stringify(trampoline(range)(1, 20000))); //=> [2, 3, 4, ..., 19997, 19998, 19999]

Using an auxiliary function can be a good technique, but here you're using your recursive function as if it were a loop.使用辅助 function 可能是一种很好的技术,但是在这里您使用递归 function 就好像它是一个循环一样。 For something like this a recursive function isn't needed, as a simple loop would do the trick.对于这样的事情,不需要递归 function,因为一个简单的循环就可以了。 With that being said, usually, when you write a recursive function, it should build up a result gradually as you return from it, while simultaneously shrinking the input arguments towards the base-case / termination clause of your function (ie: a part of your function that no longer contains any recursive function calls).话虽如此,通常,当您编写递归 function 时,它应该在您从它返回时逐渐建立一个结果,同时将输入 arguments 缩小到 ZC1C425268E68385D1AB507 的基本情况/终止子句:您的 function 不再包含任何递归 function 调用)。 So rather than thinking about how you can go about populating one global array, try and think about how you can build your result through joining sub-results of your recursive function.因此,与其考虑如何使用 go 填充一个全局数组,不如尝试考虑如何通过连接递归 function 的子结果来构建结果。 Eg:例如:

//    a  b     a+1                    a+1    b
range(2, 6) = [ 3 ] joined with range(3,     6)                                    
                               |---------------|
 | ----------------------------|
 V
range(3, 6) = [ 4 ] joined with range(4,     6)
range(4, 6) = [ 5 ] joined with range(5,     6)
range(5, 6) = [] -----------------/\ (once base case is hit, we can work our way back up)

With that, each call to range() produces a new array with a further call to the range function.这样,每次调用range()都会生成一个新数组,并进一步调用范围 function。 Once you reach your base case you can return an empty array, which will then be returned to where the function was called and joined with the previous call to range() .达到基本情况后,您可以返回一个空数组,然后将其返回到调用 function 的位置,并与之前对range()的调用一起加入。

 function range(x, y) { if(x+1 === y) return []; return [x+1].concat(range(x+1, y)); // /\--- "joined with" } console.log(range(2, 9))

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM