蹦床递归堆栈溢出

时间:2018-05-23 14:00:18

标签: javascript recursion ecmascript-6

我有这个递归函数sum,它计算传递给它的所有数字的总和。



function sum(num1, num2, ...nums) {
  if (nums.length === 0) { return num1 + num2; }
  return sum(num1 + num2, ...nums);
}

let xs = [];
for (let i = 0; i < 100; i++) { xs.push(i); }
console.log(sum(...xs));

xs = [];
for (let i = 0; i < 10000; i++) { xs.push(i); }
console.log(sum(...xs));
&#13;
&#13;
&#13;

如果只有少数几个&#39;它可以正常工作。数字传递给它,但否则会溢出call stack。所以我尝试稍微修改它并使用trampoline以便它可以接受更多参数。

&#13;
&#13;
function _sum(num1, num2, ...nums) {
  if (nums.length === 0) { return num1 + num2; }
  return () => _sum(num1 + num2, ...nums);
}

const trampoline = fn => (...args) => {
  let res = fn(...args);
  while (typeof res === 'function') { res = res(); }
  return res;
}

const sum = trampoline(_sum);

let xs = [];
for (let i = 0; i < 10000; i++) { xs.push(i); }
console.log(sum(...xs));

xs = [];
for (let i = 0; i < 100000; i++) { xs.push(i); }
console.log(sum(...xs));
&#13;
&#13;
&#13;

虽然第一个版本不能处理10000个数字,但第二个版本是。但是,如果我将100000个号码传递给第二个版本,我又会再次出现call stack overflow错误。

我会说100000并不是那么大(这里可能有问题)并且没有看到任何可能导致内存泄漏的失控闭包。

有谁知道它有什么问题吗?

3 个答案:

答案 0 :(得分:3)

另一个答案指出了函数参数数量的限制,但我想谈谈你的trampoline实现。我们正在运行的长计算可能想要返回一个函数。如果您使用typeof res === 'function',则无法再将函数计算为返回值!

相反,使用某种唯一标识符编码您的蹦床变体

const bounce = (f, ...args) =>
  ({ tag: bounce, f: f, args: args })

const done = (value) =>
  ({ tag: done, value: value })

const trampoline = t =>
{ while (t && t.tag === bounce)
    t = t.f (...t.args)
  if (t && t.tag === done)
    return t.value
  else
    throw Error (`unsupported trampoline type: ${t.tag}`)
}

在我们开始之前,让我们首先得到一个示例函数来修复

const none =
  Symbol ()

const badsum = ([ n1, n2 = none, ...rest ]) =>
  n2 === none
    ? n1
    : badsum ([ n1 + n2, ...rest ])

我们会在其上抛出一个range个数字,以确保其正常工作

const range = n =>
  Array.from
    ( Array (n + 1)
    , (_, n) => n
    )

console.log (range (10))
// [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

console.log (badsum (range (10)))
// 55

但它可以处理大联盟吗?

console.log (badsum (range (1000)))
// 500500

console.log (badsum (range (20000)))
// RangeError: Maximum call stack size exceeded

到目前为止,请在浏览器中查看结果

const none =
  Symbol ()

const badsum = ([ n1, n2 = none, ...rest ]) =>
  n2 === none
    ? n1
    : badsum ([ n1 + n2, ...rest ])

const range = n =>
  Array.from
    ( Array (n + 1)
    , (_, n) => n
    )

console.log (range (10))
// [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

console.log (badsum (range (1000)))
// 500500

console.log (badsum (range (20000)))
// RangeError: Maximum call stack size exceeded

介于1000020000之间的badsum函数不出所料导致堆栈溢出。

除了将函数重命名为goodsum之外,我们只需使用我们的trampoline变体对返回类型进行编码

const goodsum = ([ n1, n2 = none, ...rest ]) =>
  n2 === none
    ? n1
    ? done (n1)
    : goodsum ([ n1 + n2, ...rest ])
    : bounce (goodsum, [ n1 + n2, ...rest ])

console.log (trampoline (goodsum (range (1000))))
// 500500

console.log (trampoline (goodsum (range (20000))))
// 200010000
// No more stack overflow!

您可以在此处在浏览器中查看此程序的结果。现在我们可以看到递归和蹦床都没有错误,因为这个程序很慢。不过不用担心,我们稍后会解决这个问题。

const bounce = (f, ...args) =>
  ({ tag: bounce, f: f, args: args })
  
const done = (value) =>
  ({ tag: done, value: value })
  
const trampoline = t =>
{ while (t && t.tag === bounce)
    t = t.f (...t.args)
  if (t && t.tag === done)
    return t.value
  else
    throw Error (`unsupported trampoline type: ${t.tag}`)
}

const none =
  Symbol ()

const range = n =>
  Array.from
    ( Array (n + 1)
    , (_, n) => n
    )

const goodsum = ([ n1, n2 = none, ...rest ]) =>
  n2 === none
    ? done (n1)
    : bounce (goodsum, [ n1 + n2, ...rest ])

console.log (trampoline (goodsum (range (1000))))
// 500500

console.log (trampoline (goodsum (range (20000))))
// 200010000
// No more stack overflow!

trampoline的额外调用会让人讨厌,当你单独看goodsum时,donebounce在那里做什么并不是很明显,除非这是许多课程中非常常见的惯例。

我们可以使用通用loop函数更好地编码循环意图。循环被赋予一个函数,只要函数调用recur,就会调用该函数。它看起来像一个递归调用,但实际上recur正在构建一个loop以堆栈安全的方式处理的值。

我们为loop提供的函数可以包含任意数量的参数,并具有默认值。这也很方便,因为我们现在可以通过简单地使用初始化为...的索引参数i来避免昂贵的0解构和传播。函数的调用者无法在循环调用之外访问这些变量

此处的最后一个优点是,goodsum的读者可以清楚地看到循环编码,并且不再需要显式done标记。该函数的用户无需担心调用trampoline,因为它已在loop

中为我们处理过
const goodsum = (ns = []) =>
  loop ((sum = 0, i = 0) =>
    i >= ns.length
      ? sum
      : recur (sum + ns[i], i + 1))

console.log (goodsum (range (1000)))
// 500500

console.log (goodsum (range (20000)))
// 200010000

console.log (goodsum (range (999999)))
// 499999500000

现在是我们的looprecur对。这次我们使用标记模块扩展我们的{ tag: ... }约定

const recur = (...values) =>
  tag (recur, { values })

const loop = f =>
{ let acc = f ()
  while (is (recur, acc))
    acc = f (...acc.values)
  return acc
}

const T =
  Symbol ()

const tag = (t, x) =>
  Object.assign (x, { [T]: t })

const is = (t, x) =>
  t && x[T] === t

在浏览器中运行以验证结果

const T =
  Symbol ()

const tag = (t, x) =>
  Object.assign (x, { [T]: t })

const is = (t, x) =>
  t && x[T] === t

const recur = (...values) =>
  tag (recur, { values })

const loop = f =>
{ let acc = f ()
  while (is (recur, acc))
    acc = f (...acc.values)
  return acc
}

const range = n =>
  Array.from
    ( Array (n + 1)
    , (_, n) => n
    )

const goodsum = (ns = []) =>
  loop ((sum = 0, i = 0) =>
    i >= ns.length
      ? sum
      : recur (sum + ns[i], i + 1))

console.log (goodsum (range (1000)))
// 500500

console.log (goodsum (range (20000)))
// 200010000

console.log (goodsum (range (999999)))
// 499999500000

<强>额外

我的大脑已被困在变形装置中几个月了,我很好奇是否可以使用上面介绍的unfold函数实现堆栈安全loop

下面,我们看一个示例程序,它生成总和最多n的整个序列。可以将其视为显示上述goodsum程序答案的工作。总计n的总和是数组中的最后一个元素。

这是unfold的一个很好的用例。我们可以直接使用loop来写这个,但重点是延长unfold的限制,所以这里去了

const sumseq = (n = 0) =>
  unfold
    ( (loop, done, [ m, sum ]) =>
        m > n
          ? done ()
          : loop (sum, [ m + 1, sum + m ])
    , [ 1, 0 ]
    )

console.log (sumseq (10))
// [ 0,   1,   3,   6,   10,  15,  21,  28,  36, 45 ]
//   +1 ↗ +2 ↗ +3 ↗ +4 ↗ +5 ↗ +6 ↗ +7 ↗ +8 ↗ +9 ↗  ...

如果我们使用了不安全的unfold实现,我们就可能会破坏堆栈

// direct recursion, stack-unsafe!
const unfold = (f, initState) =>
  f ( (x, nextState) => [ x, ...unfold (f, nextState) ]
    , () => []
    , initState
    )

console.log (sumseq (20000))
// RangeError: Maximum call stack size exceeded

稍微玩一下之后,确实可以使用我们的堆栈安全unfoldloop进行编码。使用...效果清理push点差语法会使事情变得更快

const push = (xs, x) =>
  (xs .push (x), xs)

const unfold = (f, init) =>
  loop ((acc = [], state = init) =>
    f ( (x, nextState) => recur (push (acc, x), nextState)
      , () => acc
      , state
      ))

使用堆栈安全unfold,我们的sumseq功能现在可以处理

console.time ('sumseq')
const result = sumseq (20000)
console.timeEnd ('sumseq')

console.log (result)
// sumseq: 23 ms
// [ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ..., 199990000 ]

在浏览器中验证结果

const recur = (...values) =>
  tag (recur, { values })

const loop = f =>
{ let acc = f ()
  while (is (recur, acc))
    acc = f (...acc.values)
  return acc
}

const T =
  Symbol ()

const tag = (t, x) =>
  Object.assign (x, { [T]: t })

const is = (t, x) =>
  t && x[T] === t

const push = (xs, x) =>
  (xs .push (x), xs)

const unfold = (f, init) =>
  loop ((acc = [], state = init) =>
    f ( (x, nextState) => recur (push (acc, x), nextState)
      , () => acc
      , state
      ))

const sumseq = (n = 0) =>
  unfold
    ( (loop, done, [ m, sum ]) =>
        m > n
          ? done ()
          : loop (sum, [ m + 1, sum + m ])
    , [ 1, 0 ]
    )

console.time ('sumseq')
const result = sumseq (20000)
console.timeEnd ('sumseq')

console.log (result)
// sumseq: 23 ms
// [ 0, 1, 3, 6, 10, 15, 21, 28, 36, 45, ..., 199990000 ]

答案 1 :(得分:2)

Browsers have practical limits on the number of arguments a function can take

您可以更改sum签名以接受数组而不是不同数量的参数,并使用解构来保持语法/可读性与您拥有的相似。这“修复”了stackoverflow错误,但速度递增缓慢:D

function _sum([num1, num2, ...nums]) { /* ... */ }

即:如果你遇到最大参数计数的问题,你的递归/蹦床方法可能会太慢而无法使用......

答案 2 :(得分:2)

另一个答案已经解释了您的代码问题。这个答案表明,对于大多数基于阵列的计算,蹦床足够快,并提供更高级别的抽象:

// trampoline

const loop = f => {
  let acc = f();

  while (acc && acc.type === recur)
    acc = f(...acc.args);

  return acc;
};


const recur = (...args) =>
  ({type: recur, args});


// sum

const sum = xs => {
  const len = xs.length;

  return loop(
    (acc = 0, i = 0) =>
      i === len
        ? acc
        : recur(acc + xs[i], i + 1));
};


// and run...

const xs = Array(1e5)
  .fill(0)
  .map((x, i) => i);


console.log(sum(xs));

如果基于蹦床的计算导致性能问题,那么您仍然可以用裸循环替换它。