基于F#continuation的尾递归

时间:2017-05-21 11:30:02

标签: f# tail-recursion continuation-passing

在终止基于延续的尾递归函数时,有人可以澄清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表示整数。

3 个答案:

答案 0 :(得分:8)

虽然其他答案给出了以延续传递方式编写函数的良好背景,但他们错过了一个重要的观点,在我看来,它也更容易理解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(用于累加器的常用类型)没有什么不同,除了你可以调用它给它初始值。

答案 1 :(得分:5)

编辑:这称为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混淆,因为https://sourceforge.net/p/linux-ima/wiki/Home/完全不同,而且功能强大得多(不确定F#是否有它们)。

答案 2 :(得分:2)

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

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

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