【问题标题】:How can I make a retry function tail recursive?如何使重试函数尾递归?
【发布时间】:2016-05-03 13:19:16
【问题描述】:

我有一个与Scott's Railway Oriented Programming 中使用的Result 类型类似的可区分联合。为简单起见,这里稍微简化一下:

type ErrorMessage = ErrorMessage of string

type ValidationResult<'a> =
    | Success of 'a
    | Error of ErrorMessage

我有一个相应的模块ValidationResult,其中包含作用于这些ValidationResults 的函数,其中一个是递归retryable 函数,它允许再次调用参数f: unit -&gt; 'a(例如读取来自stdin) 如果ValidationResultError

module ValidationResult

    let doubleMap success error = function
         | Success x -> success x
         | Error e -> error e

    let rec retryable errorHandler f =
        let result = f ()
        let retry e =
            errorHandler e
            retryable errorHandler f
        doubleMap id retry result

但它不是尾递归的,我想将其转换为尾递归。我该怎么做?

【问题讨论】:

  • AFAIK,函数本身是尾递归的,但编译器不会将其编译成循环,因为它使用嵌套函数 - 所以您需要在项目选项中启用“尾调用” - 这样,它将生成带有实际尾调用指令的 .NET 代码。
  • 但如果你在 Windows 或 .NET Core 上运行,JIT 甚至不需要实际的尾调用指令,它无论如何都会优化尾调用。
  • @TomasPetricek 我想如果我将我的代码反编译成 C# 并看到一个 while (true) 循环,这意味着我的代码是尾递归的,这在我的 doubleMap 版本中不会发生
  • @rexcfnghk 我在答案中添加了更多细节。

标签: recursion f# tail-recursion


【解决方案1】:

F# 编译器以两种不同的方式编译尾递归函数。

  1. 如果函数很简单(直接调用自身),则编译成循环
  2. 如果尾递归涉及多个不同的函数(甚至函数值),则编译器使用.tail IL 指令进行尾调用。这也是尾调用,但由 .NET 运行时处理,而不是由 F# 编译器消除。

在您的情况下,retryable 函数 已经是尾递归的,但它是第二种。丹尼尔的回答让它变得足够简单,以至于它成为第一种。

但是,您可以保持函数原样,它将是尾递归的。唯一需要注意的是,编译器在调试模式下默认不会生成.tail 指令(因为它会弄乱调用堆栈),因此您需要显式启用它(在项目选项中,选中“生成尾调用” )。

【讨论】:

  • 了解非常有用!
【解决方案2】:

只需删除对doubleMap 的调用即可:

let rec retryable errorHandler f =
    match f() with
    | Success x -> x
    | Error e ->
        errorHandler e
        retryable errorHandler f

【讨论】:

  • 只是好奇,有什么方法可以保持对doubleMap 的调用,同时仍然使其尾递归?
  • 我还没有测试过,但是根据 Tomas 的说法,它应该是尾递归的。您是否尝试过发布版本?在调试版本中默认禁用尾部调用。
  • IMO,没有内部函数更容易理解。
  • 我想如果我将我的代码反编译成 C# 并看到一个 while (true) 循环,这意味着我的代码是尾递归的,这在我的 doubleMap 版本中不会发生
猜你喜欢
  • 2020-09-16
  • 2018-09-03
  • 1970-01-01
  • 2016-01-14
  • 1970-01-01
  • 2019-09-14
  • 2020-01-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多