F#/Scala 中优化相互递归的标准方法是什么?

发布于 2024-09-01 01:28:38 字数 111 浏览 3 评论 0原文

这些语言不支持“本机”相互递归函数优化,所以我猜它一定是蹦床或..呵呵..重写为循环)我错过了什么吗?

更新:看来我确实对 FSharp 撒了谎,但我只是在谷歌搜索时没有看到相互尾部调用的例子

These languages do not support mutually recursive functions optimization 'natively', so I guess it must be trampoline or.. heh.. rewriting as a loop) Do I miss something?

UPDATE: It seems that I did lie about FSharp, but I just didn't see an example of mutual tail-calls while googling

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

硬不硬你别怂 2024-09-08 01:28:38

首先,F# 本身支持相互递归函数,因为它可以受益于 .NET IL 中提供的 tailcall 指令 (MSDN)。然而,这有点棘手,并且可能不适用于 .NET 的某些替代实现(例如 Compact Frameworks),因此您有时可能需要手动处理这个问题。

一般来说,我认为有几种方法可以处理它:

  • Trampoline - 当递归深度太高时抛出异常并实现处理异常的顶级循环(异常将携带恢复呼叫的信息)。除了异常之外,您还可以简单地返回一个值,指定应再次调用该函数。

  • 使用计时器展开 - 当递归深度太高时,您创建一个计时器并给它一个回调,计时器将在很短的时间后调用该回调(计时器将继续递归) ,但使用的堆栈将被丢弃)。

    使用存储需要完成的工作的全局堆栈可以完成同样的事情。您可以将函数添加到堆栈中,而不是调度计时器。在顶层,程序将从堆栈中选择函数并运行它们。

为了给出第一种技术的具体示例,在 F# 中您可以这样写:

type Result<´T> =
  | Done of ´T
  | Call of (unit -> ´T)

let rec factorial acc n = 
  if n = 0 then Done acc
  else Call(fun () -> factorial (acc * n) (n + 1))

这也可以用于相互递归函数。命令式循环将简单地调用存储在 Call(f) 中的 f 函数,直到生成具有最终结果的 Done。我认为这可能是实现这一点的最干净的方法。

我确信还有其他复杂的技术可以解决这个问题,但这些是我所知道的(并且我使用过的)。

First of all, F# supports mutually recursive functions natively, because it can benefit from the tailcall instruction that's available in the .NET IL (MSDN). However, this is a bit tricky and may not work on some alternative implementations of .NET (e.g. Compact Frameworks), so you may sometimes need to deal with this by hand.

In general, I that there are a couple of ways to deal with it:

  • Trampoline - throw an exception when the recursion depth is too high and implement a top-level loop that handles the exception (the exception would carry information to resume the call). Instead of exception you can also simply return a value specifying that the function should be called again.

  • Unwind using timer - when the recursion depth is too high, you create a timer and give it a callback that will be called by the timer after some very short time (the timer will continue the recursion, but the used stack will be dropped).

    The same thing could be done using a global stack that stores the work that needs to be done. Instead of scheduling a timer, you would add function to the stack. At the top-level, the program would pick functions from the stack and run them.

To give a specific example of the first technique, in F# you could write this:

type Result<´T> =
  | Done of ´T
  | Call of (unit -> ´T)

let rec factorial acc n = 
  if n = 0 then Done acc
  else Call(fun () -> factorial (acc * n) (n + 1))

This can be used for mutually recursive functions as well. The imperative loop would simply call the f function stored in Call(f) until it produces Done with the final result. I think this is probably the cleanest way to implement this.

I'm sure there are other sophisticated techniques for dealing with this problem, but those are the two I know about (and that I used).

音盲 2024-09-08 01:28:38

在 Scala 2.8 上,scala.util.control.TailCalls:

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

On Scala 2.8, scala.util.control.TailCalls:

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
南笙 2024-09-08 01:28:38

只是为了在 Bing for F# 相互递归时方便使用代码:

let rec isOdd x =
    if x = 1 then true else isEven (x-1)
and isEven x = 
    if x = 0 then true else isOdd (x-1)

printfn "%A" (isEven 10000000)

如果您在没有尾部调用的情况下进行编译(“调试”模式下的默认设置,该模式保留堆栈以便于调试),这将导致 StackOverflow,但在使用尾部调用编译时运行得很好(默认为“Release”模式)。编译器默认执行尾部调用(请参阅--tailcalls 选项) ,并且大多数平台上的 .NET 实现都遵循它。

Just to have the code handy for when you Bing for F# mutual recursion:

let rec isOdd x =
    if x = 1 then true else isEven (x-1)
and isEven x = 
    if x = 0 then true else isOdd (x-1)

printfn "%A" (isEven 10000000)

This will StackOverflow if you compile without tail calls (the default in "Debug" mode, which preserves stacks for easier debugging), but run just fine when compiled with tail calls (the default in "Release" mode). The compiler does tail calls by default (see the --tailcalls option), and .NET implementations on most platforms honor it.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文