我在课堂上被告知,由于在递归调用之后计算了布尔运算符,以下函数不是尾递归的:
let rec exists p = function
[] -> false
| a::l -> p a || exists p l
但是,这并没有将堆栈放在一个1000万大小的列表中,更重要的是,它是标准库中的实现。如果它不是尾递归的话,就没有理由使用这个形式而不是看似等价且清晰的尾递归
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
然后堆栈确实在10米元素上爆炸。所以看起来OCaml只能在右侧出现递归调用时才能利用这一点,这让我怀疑所有编译器都会用等效的if
表达式替换boolean op。有人可以确认或反驳这个吗?
答案 0 :(得分:10)
告诉你这个人是错的。
实际上,||
不会立即转换为if / then / else,而是通过编译器的中间语言保留一点,以便轻松启用两种不同的转换:
a || b
在表达位置被翻译为if a then true else b
a || b
位于测试位置,即if a || b then c else d
被翻译成if a then goto c else if b then goto c else d
,当goto c
跳转到计算{{1}时(只需翻译成c
即可复制if a then c else if b then c
的代码)。这种优化更为神秘,用户无需了解其优化程序的性能。您可以在编译器的源代码中亲眼看到。 c
原语表示为||
,感兴趣的文件为asmcomp/cmmgen.ml,用于本机编译((1),(2)])和{{3}用于字节码编译(两个方面同时处理,通过生成的字节码指令来使用结果)。
一个小问题:你似乎说OCaml能够在“右边”优化尾调用,因为这种情况“足够简单”,但不是“在左边”,因为编译器不够聪明。如果呼叫显示在左侧,则不尾部呼叫,因此不能对其进行优化。这不是一个“简单”尾调用的问题。
最后,如果你想检查一个尾部是否是尾部调用,你可以使用OCaml工具:使用Psequor
选项进行编译将产生一个注释文件-annot
(如果你的source是foo.annot
),它包含有关程序表达式类型的信息,对于函数调用,还有关于它们是否是尾调用的信息。例如,使用Emacs中的foo.ml
,caml-mode
指向M-x caml-types-show-call
之后的exists
命令将确认我这是一个“尾调用”,而当调用时||
它返回“堆栈调用”。
答案 1 :(得分:9)
如果有人写道:
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
这不是尾递归。
答案 2 :(得分:2)
雷米的回答是完全正确的。请注意,使用与OCaml不同的键入系统的某些语言会自动将某些非布尔值强制转换为布尔值。这些语言可以选择像(||)这样的运算符:或者不尝试将rhs的结果强制转换为布尔值,但是只返回给定的任何内容,或者强制执行但是在rhs之后你还有一些工作要做被评估,所以你放弃(||)的尾递归。你不能两者兼得。也许你的线人正在思考这些问题,这就是他们错误地说出他们对OCaml做了什么的原因。鉴于OCaml对类型的严格处理,您首先不能说true || succ 5
之类的内容。