【问题标题】:Tail call conversion in OCamlOCaml 中的尾调用转换
【发布时间】:2012-07-08 00:17:31
【问题描述】:

我在课堂上被告知,以下函数不是尾递归,因为布尔运算符在递归调用之后被评估:

let rec exists p = function
    [] -> false
  | a::l -> p a || exists p l

但这并没有将一千万大小的列表炸毁,更重要的是,它是标准库中的实现。如果它不是尾递归,就没有理由使用这种形式来代替看似等效且明显的尾递归

let rec exists p = function
    [] -> false
  | a::l -> if p a then true else exists p l

所以看起来 OCaml 编译器能够在像这样的简单情况下优化布尔运算以利用尾递归。但我注意到,如果我像这样切换操作数的顺序

let rec exists p = function
    [] -> false
  | a::l -> exists p l || p a

那么堆栈确实在 10m 个元素上被炸毁。所以看起来 OCaml 只有在递归调用出现在右侧时才能利用这一点,这让我怀疑编译器所做的只是用等效的 if 表达式替换布尔运算。有人可以证实或反驳这一点吗?

【问题讨论】:

    标签: ocaml tail-recursion tail-call-optimization


    【解决方案1】:

    告诉你这件事的人是错的。

    其实||并不是马上翻译成if/then/else,而是通过编译器的中间语言保留了一点,方便实现两种不同的转换:

    1. 正如你所说,a || b 在表达式位置被翻译成if a then true else b
    2. 但是a || b在测试位置,即if a || b then c else d被不同地翻译成类似if a then goto c else if b then goto c else d的东西,当goto c是一个跳转到c的计算时(只是翻译成if a then c else if b then c会复制c的代码)。这种优化更加神秘,用户无需了解它即可推断其程序的性能。

    您可以在编译器的源代码中亲自查看。 || 原语表示为 Psequor,感兴趣的文件是 asmcomp/cmmgen.ml 用于本机编译((1)(2)]),bytecomp/bytegen.ml 用于字节码编译(这两个方面都在同时,通过指令产生的字节码使用结果)。

    一个小点:您似乎说 OCaml 能够优化尾部调用“在右侧”,因为这种情况“足够简单”,但不能“在左侧”,因为编译器不够聪明。如果调用出现在左边,它不是尾调用,所以一定不能优化。这不是一个“简单”的尾声的问题。

    最后,如果你想检查一个尾部是否是尾调用,你可以使用 OCaml 工具:使用-annot 选项编译将生成一个注释文件foo.annot(如果你的源是@987654339 @) 包含关于程序表达式类型的信息,对于函数调用,关于它们是否是尾调用。以 Emacs 中的 caml-mode 为例,M-x caml-types-show-call 命令在 || 之后指向 exists 将确认这是一个“尾调用”,而当在 p x 上调用它时,它返回“堆栈调用”。

    【讨论】:

    • 有趣。编译器通常会做这样的翻译吗?我刚刚开始 OCaml,但我肯定会更多地研究编译器源代码,因为我想了解这一点。而且我用的是图阿雷格模式,不知道有没有等价的命令?
    • 我不确定您所说的“像这样”是什么,但是是的,编译器甚至语言规范通过将某些构造转换为更原始的构造来定义它们是很常见的。我们有时称其为“语法糖”或“派生表达式”,但这仅适用于用户可见的部分;事实上,编译器一直在为各种中间语言(我已经指出你要操纵三种这样的语言的文件)在不同的级别上做到这一点。我不关心 tuareg 模式下的 -annot 支持。
    • @gasche 感谢您提供有关 -annot 的信息,我不知道这一点。
    • 我只是想知道||具体会不会这样翻译,但我想在机器码的路上会有很多复杂的翻译发生。
    【解决方案2】:

    如果有人写道:

    let rec add_result p = function
      [] -> 0
    | a::l -> p a + add_result p l
    

    这不会是尾递归,因为在递归调用之后,函数必须添加两个结果。

    但是||不是普通运算符,A || B严格等价于if A then true else B,所以当你写了

    let rec exists p = function
      [] -> false
    | a::l -> p a || exists p l
    

    一样
    let rec exists p = function
      [] -> false
    | a::l -> if p a then true else exists p l
    

    函数是尾递归的。

    let rec exists p = function
      [] -> false
    | a::l -> exists p l || p a
    

    等价于

    let rec exists p = function
      [] -> false
    | a::l -> if exist p l then true else p a
    

    这不是尾递归。

    【讨论】:

    • 这就是我真正怀疑的 - 它是编译器的直接翻译。
    【解决方案3】:

    雷米的回答完全正确。请注意,某些类型系统与 OCaml 不同的语言会自动将某些非布尔值强制转换为布尔值。这些语言可以选择使用 (||) 之类的运算符:要么不尝试将 rhs 的结果强制转换为布尔值,而是返回给定的任何内容,或者进行强制但在 rhs 之后你还有一些工作要做被评估,所以你放弃了 (||) 的尾递归。你不能两者兼得。也许你的线人是按照这些思路思考的,这就是为什么他们错误地说他们对 OCaml 做了什么。鉴于 OCaml 对类型的严格处理,您一开始就不能说出 true || succ 5 这样的东西。

    【讨论】:

    • 嗯,你能举一个这样的函数式语言的例子吗?或者你的意思是像C如何处理“布尔值”?至于我的教授,我认为他要么不知道||的转换,要么不想在课堂上详细说明。
    • code.google.com/p/pure-lang 曾经是这样强制的,但现在已经放弃了,现在用尾递归的方式做事。
    • @dubiousjim:您可以通过在预期布尔值而不是应该产生布尔值的地方强制来避免这个问题。
    猜你喜欢
    • 2013-11-15
    • 2014-06-18
    • 1970-01-01
    • 2012-09-03
    • 1970-01-01
    • 2015-12-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多