16

我写了以下函数:

let str2lst str =
    let rec f s acc =
      match s with
        | "" -> acc
        | _  -> f (s.Substring 1) (s.[0]::acc)
    f str []

我怎么知道 F# 编译器是否把它变成了循环?有没有办法在不使用 Reflector 的情况下找出答案(我没有使用 Reflector 的经验,也不懂 C#)?

编辑:另外,是否可以在不使用内部函数的情况下编写尾递归函数,或者循环是否必须驻留在其中?

此外,F# std lib 中是否有一个函数可以多次运行给定函数,每次都将最后一个输出作为输入?假设我有一个字符串,我想在字符串上运行一个函数,然后在结果字符串上再次运行它,依此类推......

4

2 回答 2

22

不幸的是,没有简单的方法。

阅读源代码并使用类型并通过检查确定某事是否是尾调用(是“最后一件事”,而不是在“尝试”块中)并不难,但人们会猜测自己和犯错误。没有简单的自动化方法(例如检查生成的代码除外)。

当然,您可以在大量测试数据上尝试您的功能,看看它是否会爆炸。

F# 编译器将为所有尾调用生成 .tail IL 指令(除非使用编译器标志来关闭它们 - 用于当您想要保留堆栈帧以进行调试时),但直接尾递归函数将被优化成循环。(编辑:我认为现在 F# 编译器在可以证明没有通过此调用站点的递归循环的情况下也无法发出 .tail ;这是一种优化,因为 .tail 操作码在许多平台上有点慢。)

'tailcall' 是保留关键字,其想法是 F# 的未来版本可能允许您编写例如

tailcall func args

如果不是尾调用,则收到警告/错误。

只有不是自然尾递归的函数(因此需要一个额外的累加器参数)才会“强制”你进入“内部函数”习语。

这是您所要求的代码示例:

let rec nTimes n f x =
    if n = 0 then
        x
    else
        nTimes (n-1) f (f x)

let r = nTimes 3 (fun s -> s ^ " is a rose") "A rose"
printfn "%s" r
于 2009-04-30T13:23:42.640 回答
3

我喜欢 Paul Graham 在On Lisp中制定的经验法则:如果还有工作要做,例如操作递归调用输出,那么调用就不是尾递归的。

于 2012-04-17T11:48:44.987 回答