如何使用TailCalls?
如果我理解正确,scala.util.control.TailCalls 可用于通过使用蹦床来避免非尾递归函数的堆栈溢出。 API 中给出的示例很简单:
import scala.util.control.TailCalls._
def isEven(xs: List[Int]): TailRec[Boolean] =
if (xs.isEmpty) done(true) else tailcall(isOdd(xs.tail))
def isOdd(xs: List[Int]): TailRec[Boolean] =
if (xs.isEmpty) done(false) else tailcall(isEven(xs.tail))
isEven((1 to 100000).toList).result
但是,更有趣的情况是,如果您想在递归调用之后执行一些操作。我以某种方式运行了一个“天真的”阶乘实现,
def fac(n:Long): TailRec[Long] =
if (n == 0) done(1) else done(n * tailcall(fac(n - 1)).result)
但这看起来很糟糕,我怀疑这是否是预期用途。所以我的问题是如何使用 TailCalls 正确编写阶乘或斐波那契函数(是的,我知道如何使用累加器使它们尾递归)?或者TailCalls不适合解决这类问题?
If I understand correctly, scala.util.control.TailCalls can be used to avoid stack overflows for non-tail-recursive functions by using a trampoline. The example given in the API is straightforward:
import scala.util.control.TailCalls._
def isEven(xs: List[Int]): TailRec[Boolean] =
if (xs.isEmpty) done(true) else tailcall(isOdd(xs.tail))
def isOdd(xs: List[Int]): TailRec[Boolean] =
if (xs.isEmpty) done(false) else tailcall(isEven(xs.tail))
isEven((1 to 100000).toList).result
However, the more interesting case is if you want to do some operations after the recursve call. I got a "naive" factorial implementation somehow running by
def fac(n:Long): TailRec[Long] =
if (n == 0) done(1) else done(n * tailcall(fac(n - 1)).result)
but this looks horrible and I doubt that this is the intended use. So my question is how to write a factorial or fibonacci function correctly using TailCalls (yes, I know how to use accumulators to get them tail-recursive)? Or is TailCalls not suitable for this kind of problem?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
是的,您的朴素阶乘不会是尾递归的,并且将在参数值中使用线性堆栈空间。 scala.util.control.TailCalls 的目的不是神奇地将非尾递归算法变成尾递归算法。它的目的是允许相互尾部调用的函数循环在恒定的堆栈空间中执行。
Scala 编译器对在尾部位置调用自身的方法实现尾递归优化,允许调用者使用调用方法的堆栈帧。它本质上是通过在幕后将可证明的尾递归调用转换为 while 循环来实现这一点的。然而,由于 JVM 的限制,它无法实现尾部调用优化,这将允许尾部位置的任何方法调用重用调用者的堆栈帧。这意味着如果有两个或多个方法在尾部位置相互调用,则不会进行任何优化,并且会有堆栈溢出的风险。 scala.util.control.TailCalls 是一个可以让你解决这个问题的 hack。
顺便说一句,非常值得查看 scala.util.control.TailCalls 的源代码。 “结果”调用是完成所有有趣工作的地方,它基本上只是内部的一个 while 循环。
Yes, your naive factorial will not be tail recursive, and will use stack space linear in the value of the argument. The purpose of scala.util.control.TailCalls is not to magically turn non-tail-recursive algorithms into tail recursive ones. It's purpose is to allow cycles of mutually tail-called functions to execute in constant stack space.
The Scala compiler implements tail-recursion optimization for methods which call themselves in tail position, allowing the stack frame of the calling method to be used by the caller. It does this essentially by converting a provably tail-recursive call to a while-loop, under the covers. However, due to JVM restrictions there's no way for it to implement tail-call optimization, which would allow any method call in tail position to reuse the caller's stack frame. This means that if you have two or more methods that call each other in tail position, no optimization will be done, and stack overflow will be risked. scala.util.control.TailCalls is a hack that allows you to work around this problem.
BTW, It's well worth looking at the source to scala.util.control.TailCalls. The "result" call is where all the interesting work gets done, and it's basically just a while loop inside.
这个问题已经有6年多了,但接受的答案似乎没有回答这个问题:
所以,这就是:
使用 TailCalls 的一大用例是做一些类似右折叠的事情。
This question is more than 6 years old, but the accepted answer doesn't seem to answer the question:
So, here it is:
One of the big use-cases for using TailCalls is to do something that is right-fold-like.