【问题标题】:JavaScript stack overflows even when no recursive function is present即使不存在递归函数,JavaScript 堆栈也会溢出
【发布时间】:2021-04-06 14:50:37
【问题描述】:

假设我们想通过使用 lambdas 作为代表我们想要添加新数据的位置的“洞”来构建大型结构。例如,这里我们使用这个想法构建[0,[1,[2,null]]]

builder_0 = hole => hole;                // hole => hole;
builder_1 = hole => builder_0([0,hole]); // hole => [0, hole];
builder_2 = hole => builder_1([1,hole]); // hole => [0, [1, hole]];
builder_3 = hole => builder_2([2,hole]); // hole => [0, [1, [2, hole]]];

// prints [0,[1,[2,null]]], but will stack overflows if too many lines
console.log(JSON.stringify(builder_3(null)));

这很好用。我们也可以循环执行:

let builder = hole => hole;
for (let i = 0; i < 1000; ++i) {
  let last_builder = builder;
  builder = hole => last_builder([i, hole]);
};
console.log(builder(null));

这也有效,但如果限制大于10000,此算法将堆栈溢出。问题是,由于last_builder([i,hole]) 没有在hole =&gt; ... 闭包内进行评估,它会构建大量未评估的lambdas,这些lambdas 将迅速消耗整个堆栈。请注意,[0,[1,[2,null]]] 只是一个无用的示例,JavaScript 将无法使用上述基于孔的技术构建任何大型结构(想想树、JSON、不可变容器等)。

尾调用优化和蹦床在这里无济于事,因为我们甚至没有一开始的递归函数。有没有什么巧妙的技巧可以让这种函数式成语在没有堆栈溢出的情况下工作?

【问题讨论】:

  • 为什么不将每个 lambda 推入数组,而不是拥有 10,000 个嵌套数组,每个数组只有一个整数和另一个数组?我不明白这个数据结构的目的。
  • 第一个 sn-p 的前两行足以说明问题,然后是第一个 sn-p 的最后一行。在第 2 行中,构建器已被重新定义为自称 ad infinitum。没有终止呼叫的终止条件。
  • @MaiaVictor 我不知道你为什么会告诉我我错了,当我使用我指出的行在浏览器控制台中展示问题时。
  • 我说“我说的不是你的要点。”
  • @Amy Jesus,我需要眼镜。所以,是的,你是对的,我的问题上的 sn-p 是错误的。我修复了它以正确演示该问题。感谢您指出错误。

标签: javascript functional-programming stack-overflow


【解决方案1】:

当您在编译函数式代码时,您可以让自己在 JS 部分中没有“函数式”。因此,您无需递归即可创建所需的数据结构:

const builder = (count,hole) => {
   let arr = [hole];
   let i = 0;
   while (count--) arr = [count,arr]
   return arr;
}

console.log(builder(22333,null))

(请在浏览器控制台试一试,代码sn-p工具无法重现)

【讨论】:

  • 真的!这在这种情况下有效,但不适用于更复杂的算法。有一些技术可以将任意递归函数转换为非函数样式,但它们需要复杂的全局程序转换。
【解决方案2】:

我意识到正确的方法是蹦床。我们只是使用直接的 lambda 来包装结果:

builder_0 = hole => () => hole;                // hole => hole;
builder_1 = hole => () => builder_0([0,hole]); // hole => [0, hole];
builder_2 = hole => () => builder_1([1,hole]); // hole => [0, [1, hole]];
builder_3 = hole => () => builder_2([2,hole]); // hole => [0, [1, [2, hole]]];

然后我们使用while 循环来剥离层而不破坏堆栈:

var result = builder(null);
while (typeof result === "function") {
  result = result();
}

这是一个完整的示例,它构建了一个包含 100000 个数字的链表:

let builder = x => x;
for (let i = 0; i < 100000; ++i) {
  let last_builder = builder;
  builder = x => (() => last_builder([i,x]));
};

var result = builder(null);
while (typeof result === "function") {
  result = result();
}

console.log(result);

它不会堆栈溢出。

【讨论】:

    【解决方案3】:

    您可以使用三个函数进行回避。

    const
        identity = value => value,
        structure = (number, value) => [number, value],
        construct = (number, value) => number--
            ? construct(number, structure(number, value))
            : value;
        
    console.log(JSON.stringify(construct(1000, null)));

    【讨论】:

    • 遗憾的是,对于足够大的 nums,这仍然会溢出。
    • 突破 10k
    【解决方案4】:

    我不确定这是否适合您的需求,但您可以运行“lambda 组合”,在数组上调用 reducer,并使用您想要插入结构的值。

    Reduce 为数组中的每个条目调用提供的函数,将返回“累积”到一个变量中

    const build = (last_o,new_v) => [new_v,last_o]
    const values = [4,3,2,1,0]
    const str = values.reduce(build,null)
    console.log('Structure: ',str)
    

    【讨论】:

      猜你喜欢
      • 2017-09-06
      • 2017-10-20
      • 2018-05-28
      • 2016-03-23
      • 2011-02-26
      • 1970-01-01
      相关资源
      最近更新 更多