【问题标题】:F# continuation-based tail recursionF# 基于延续的尾递归
【发布时间】:2017-05-21 11:30:01
【问题描述】:

有人可以澄清在终止基于延续的尾递归函数时需要acc "",如下例所示:

let rec repeat_cont i s acc = 
if i = 0 then acc ""
else repeat_cont (i-1) s (fun x -> acc(s + x))

repeat_cont 4 "xo" id
val it : string = "abababab"

如果结果是一个列表,它会是acc []acc 0 是整数。

【问题讨论】:

    标签: f# tail-recursion continuation-passing


    【解决方案1】:

    虽然其他答案提供了以延续传递风格编写函数的良好背景,但他们错过了一个重要的点,在我看来这也更容易理解 CPS 的工作原理:

    在基本情况下不需要调用延续。也就是说,终止递归时不需要acc ""

    我相信您了解通过一系列递归调用传递累加器并以这种方式逐渐构建数据结构的习惯用法 - 比如说一个列表或一棵树。 CPS 没有什么不同,除了你在累加器中建立的结构是一个函数。而且由于我们使用的是函数式语言,因此在基本情况下返回的值与其他任何值一样好。

    比较下面的例子:

    let inline repeat_cont i s =
        let rec inner i s acc = 
            if i = 0 
                then acc
                else inner (i-1) s (fun x -> acc(s + x))
        inner i s id
    
    let res1: string -> string = repeat_cont 4 "xo"  
    res1 ""   // "xoxoxoxo"
    res1 "ab" // "xoxoxoxoab"
    
    let res2: int -> int = repeat_cont 4 1 
    res2 0 // 4 
    res2 5 // 9
    

    我重写了repeat_cont 以使用内部递归函数,以使其与 fsi 中的内联一起工作,否则它的代码非常相似。你会看到它的类型是int -> 'a -> ('b -> 'b),即你得到一个函数作为结果。从某种意义上说,这与返回一个列表或一个 int(用于累加器的常用类型)没有什么不同,除了您可以调用它并为其赋予初始值。

    【讨论】:

    • 也许还显示res1 "ab" => "xoxoxoxoab",即res1代表一个字符串前缀,回复:difference-lists。您确实为整数显示了这一点。所以res2 = (4 +)res1 = ("xoxoxoxo" +)。不错。
    • the structure you build up in the accumulator is a function Oooooooohhhhh... 看,现在我什至觉得问起来很愚蠢。现在完全有道理。解释得很好!
    • @WillNess:当然,为什么不呢。
    • @KagemandAndersen:你不应该这样,这是一个很好的问题。 CPS 是您真正需要投入时间和精力的事情之一,然后才能完全欣赏它(我也不会声称我自己对此很满意)。
    • @KagemandAndersen 所以现在,repeat_cont 4 "xo" 不是由 4 个 "xo"s 组成的字符串;它是一个由 4 个"xo"s 组成的字符串前缀如果你想要的是一个字符串,你必须总是""参数调用这个“字符串前缀”函数,这个实现细节更好隐藏 在基本情况下,而不是那样暴露。无论您在基本案例内部还是外部调用它,您仍然可以调用它,以应用函数链并获取实际的有形价值。
    【解决方案2】:

    编辑:这被称为continuation-passing style。每个递归调用都会构建其延续函数并将其传递给下一个递归调用,以供该调用选择使用(取决于它是否是基本情况)。


    只需写下减少步骤:

    repeat_cont 4 "xo" id
    repeat_cont 3 "xo" k1     where   k1 x = id ("xo" + x)
    repeat_cont 2 "xo" k2     where   k2 x = k1 ("xo" + x)
    repeat_cont 1 "xo" k3     where   k3 x = k2 ("xo" + x)
    repeat_cont 0 "xo" k4     where   k4 x = k3 ("xo" + x)
    k4 ""
    k3 ("xo" + "")
    k2 ("xo" + ("xo" + ""))
    k1 ("xo" + ("xo" + ("xo" + "")))
    id ("xo" + ("xo" + ("xo" + ("xo" + ""))))
    "xoxoxoxo"
    

    每个延续函数ki 是“如何处理从递归调用中收到的结果”。

    递归的情况,ki,说“无论我得到什么递归结果x,在它前面加上s,并将放大的字符串作为新的修改后的结果传递到链上”。

    最外面的id 只是说“按原样返回(最终)结果”。

    当达到0 的基本情况时,k4 延续函数已经构建并准备好接收它的参数,完成它的工作。它将"xo" 字符串添加到它的参数中,并将结果沿着延续函数链向上传递给k3。该参数将用于"xo" + x,因此它必须是一个字符串。

    在字符串中添加"" 是一个标识操作,因此基本情况是“让连续函数链完成它们的工作,而不受我的进一步干扰”。

    注意:我一直谨慎地说“延续功能”,以避免与完全不同且更强大的野兽 first-class continuations 混淆(但不确定 F# 是否有它们)。

    【讨论】:

      【解决方案3】:

      在构建列表时,元素的类型与acc 的结果相同。

      要终止递归,您需要一个基本情况,因此您使用已知值调用acc 以生成具有正确类型的内容。

      鉴于在您的示例中,acc = id,您可以将 acc "" 替换为 ""

      【讨论】:

      • id 不是在底部触发,而是在顶部触发(有关缩减顺序,请参阅我的答案)。仅将 acc "" 替换为 "" 意味着根本不会使用延续,而对于任何 n,将始终返回 ""
      猜你喜欢
      • 1970-01-01
      • 2015-08-10
      • 1970-01-01
      • 2017-03-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多