我正在我的 JavaScript 中尝试一种更函数式的风格;因此,我用 map 和 reduce 等实用函数替换了 for 循环。但是,我还没有找到 while 循环的功能替代品,因为尾调用优化通常不适用于 JavaScript。 (据我了解,ES6 可以防止尾部调用溢出堆栈,但不会优化它们的性能。)
我在下面解释了我尝试过的方法,但总而言之:如果我没有尾调用优化,那么实现 while 循环的功能方法是什么?
我试过的:
创建一个“while”效用函数:
function while(func, test, data) {
const newData = func(data);
if(test(newData)) {
return newData;
} else {
return while(func, test, newData);
}
}
由于尾调用优化不可用,我可以将其重写为:
function while(func, test, data) {
let newData = *copy the data somehow*
while(test(newData)) {
newData = func(newData);
}
return newData;
}
然而在这一点上,感觉我已经让我的代码对于使用它的任何其他人来说变得更加复杂/混乱,因为我不得不绕过一个自定义实用程序函数。我看到的唯一实际优势是它迫使我使循环变得纯净;但似乎只使用常规 while 循环并确保我保持一切纯净会更直接。
我还试图找出一种方法来创建一个生成器函数,该函数模拟递归/循环的效果,然后使用诸如 find 或 reduce 之类的实用函数对其进行迭代。但是,我还没有找到一种可读的方法来做到这一点。
最后,用实用函数替换 for 循环使我试图完成的事情更加明显(例如,对每个元素做一件事,检查每个元素是否通过测试等)。然而,在我看来,一个 while 循环已经表达了我想要完成的事情(例如,迭代直到我们找到一个质数,迭代直到答案被充分优化,等等)。
所以在这之后,我的总体问题是:如果我需要一个 while 循环,我正在以函数式风格编程,并且我无法访问尾调用优化,那么什么是最好的策略。
原文由 David Moneysmith 发布,翻译遵循 CC BY-SA 4.0 许可协议
JavaScript 中的一个例子
下面是一个使用 JavaScript 的示例。目前,大多数浏览器不支持尾调用优化,因此以下代码片段将失败
蹦床
我们可以通过改变我们写 repeat 的方式来解决这个限制,但只是稍微改变一下。我们将返回两种蹦床类型中的一种,而不是直接或立即重复返回值,
Bounce
或Done
。然后我们将使用我们的trampoline
函数来处理循环。柯里化 也会稍微减慢速度,但我们可以使用递归的辅助函数来补救。这也很好,因为它隐藏了蹦床实现细节,并且不希望调用者反弹返回值。这运行速度大约是上面的两倍
repeat
Clojure 风格
loop
/recur
Trampolines 很好,但不得不担心调用
trampoline
函数的返回值有点烦人。我们看到另一种方法是使用辅助助手,但这也有点烦人。我相信你们中的一些人也不太热衷于Bounce
和Done
包装器。Clojure 创建了一个使用一对函数的专用蹦床接口,
loop
和recur
这个串联对有助于非常优雅地表达程序哦,它也非常快
最初这种风格会让人觉得很陌生,但随着时间的推移,我发现它在制作持久的程序时是最一致的。下面的评论有助于您轻松使用表达式丰富的语法 -
延续单子
不过,这是我最喜欢的主题之一,所以我们将通过 continuation monad 看看它是什么样子。 Reusing
loop
andrecur
, we implement a stack-safecont
that can sequence operations usingchain
and run operation sequences usingrunCont
。对于repeat
,这是毫无意义的(而且很慢),但是在这个简单的例子中看到cont
的机制是很酷的 -Y
组合器Y组合器是我的精神组合器;如果不在其他技术中占有一席之地,这个答案将是不完整的。然而,Y 组合器的大多数实现都不是堆栈安全的,如果用户提供的函数重复出现太多次,将会溢出。由于这个答案都是关于保留堆栈安全行为的,所以我们当然会以安全的方式实施
Y
再次依赖我们可信赖的蹦床。Y
演示了扩展易于使用、堆栈安全、同步无限递归的能力,而不会弄乱您的函数。while
循环的实用性但老实说:当我们忽略一个更明显的潜在解决方案时,这是很多仪式:使用
for
或while
循环,但将其隐藏在功能接口后面出于所有意图和目的,这个
repeat
函数的工作方式与上面提供的函数相同——除了这个函数快了大约一倍或两倍(除了loop
/recur
解决方案)。哎呀,可以说它也更容易阅读。诚然,这个函数可能是一个人为的例子——并不是所有的递归函数都可以很容易地转换为
for
或while
循环,但在可能的情况下,它可能是最好的就这样去做。当简单的循环不起作用时,将蹦床和延续留给繁重的工作。setTimeout
不是堆栈溢出问题的解决方案好的,它 确实 有效,但只是自相矛盾。如果你的数据集很小,你不需要
setTimeout
因为不会有堆栈溢出。如果你的数据集很大并且你使用setTimeout
作为一种安全的递归机制,你不仅无法从你的函数同步返回一个值,而且它会慢得你甚至不想使用你的功能有些人找到了一个 鼓励这种可怕策略的 面试问答准备网站
What our
repeat
would look like usingsetTimeout
– notice it’s also defined in continuation passing style – ie, we must callrepeat
with a callback (k
) 得到最终值我怎么强调这有多糟糕都不为过。甚至
1e5
运行时间太长,以至于我放弃了测量它的尝试。我没有将其包含在下面的基准测试中,因为它太慢了,甚至不能被认为是一种可行的方法。承诺
Promises 具有链接计算的能力并且是堆栈安全的。然而,使用 Promises 实现堆栈安全
repeat
意味着我们将不得不放弃我们的同步返回值,就像我们使用setTimeout
一样。我将其作为“解决方案”提供,因为它 确实 解决了问题,与setTimeout
不同,但与蹦床或延续 monad 相比,它以一种非常简单的方式。正如您可能想象的那样,性能有些差,但远不及上面的示例setTimeout
差值得注意的是,在此解决方案中,Promise 实现细节对调用者完全隐藏。单个延续作为第四个参数提供,并在计算完成时调用。
基准
说真的,
while
循环快了 _很多_——几乎快了 100 倍(当比较最好和最差时——但不包括异步答案:setTimeout
和Promise
f64d1)石器时代的JavaScript
上面的技术是使用更新的 ES6 语法演示的,但是你可以在尽可能早的 JavaScript 版本中实现一个蹦床——它只需要
while
和一流的功能下面,我们使用石器时代的 javascript 来演示无限递归是可能的,并且在 不牺牲 同步返回值的情况下是高效的——在 6 秒内执行 100,000,000 次 递归——这与
setTimeout
相比是一个巨大的差异 --- 只能进行 1,000 次递归同样的时间。使用石器时代 JavaScript 的非阻塞无限递归
如果 出于某种原因,你想要非阻塞(异步)无限递归,我们可以依靠
setTimeout
在计算开始时推迟 _一个帧_。该程序还使用石器时代的 javascript 并在 8 秒内计算 100,000,000 次递归,但这次是以非阻塞方式进行的。这表明非阻塞要求并没有什么特别之处。 A
while
循环和一等函数仍然是在不牺牲性能的情况下实现堆栈安全递归的唯一基本要求在现代程序中,给定 Promise,我们将替换
setTimeout
对单个 Promise 的调用。