我在F#中使用定点组合器时出现问题:
let rec fix f a = f (fix f) a
fix (fun body num ->
if num = 1000000
then System.Console.WriteLine "Done!"
else body (num + 1)
) 0
(此代码仅用于演示问题,它是专门编写的,因此生成的IL代码易于阅读。)
此代码 - 在使用优化和尾调功能进行编译时 - 会导致StackOverflowException
。我查看了IL代码,可以在调用fix
:
.method assembly static void f@1 (class FSharpFunc`2<int32, class Unit> body,int32 num)
{
ldarg.1
ldc.i4 1000000
bne.un.s IL_0014
ldstr "Done!"
call void Console::WriteLine(string)
ret
IL_0014: ldarg.0 // Load the 'body' function onto the stack.
ldarg.1 // Load num onto the stack.
ldc.i4.1
add
// Invoke the 'body' function with num+1 as argument.
callvirt instance !1 class FSharpFunc`2<int32, class Unit>::Invoke(!0)
// Throw away the unit result.
pop
ret
}
(我稍微修改了代码,因此更容易阅读。)
StackOverflowException
的原因是对body
的调用不是尾调用(底部的callvirt
指令)。原因是编译器创建了一个实际返回Unit
的lambda调用!
所以在C#术语中:正文是Func<Int32,Unit>
,它应该是Action<Int32>
。由于调用返回了必须丢弃的内容,因此它不能是一个尾调用。另请注意,方法f@1
编译为void
,而不是Unit
,这是调用参数的结果必须被丢弃的原因。
这是真的意图还是我可以做些什么呢? 编译器处理这个lambda的方式使得定点组合器无法用于我打算使用它的所有目的。
我只想添加一下,只要你返回一些结果,它就可以了。只有不返回任何内容的函数才能按预期工作。
这有效:
let rec fix f a = f (fix f) a
fix (fun body num ->
if num = 1000000
then System.Console.WriteLine "Done!"; 0
else body (num + 1)
) 0
|> ignore
现在这是为lambda生成的代码:
.method assembly static int32 f@11 (class FSharpFunc`2<int32, int32> body, int32 num)
{
ldarg.1
ldc.i4 1000000
bne.un.s IL_0015
ldstr "Done!"
call void Console::WriteLine(string)
ldc.i4.0
ret
IL_0015: ldarg.0
ldarg.1
ldc.i4.1
add
tail.
callvirt instance !1 class FSharpFunc`2<int32, int32>::Invoke(!0)
ret
}
现在有一个尾叫。一切正常。
fix
的IL代码(供评论中讨论):
.method public static !!b fix<a, b> (class FSharpFunc`2<class FSharpFunc`2<!!a, !!b>, class FSharpFunc`2<!!a, !!b>> f, !!a a)
{
ldarg.0
ldarg.0
newobj instance void class Program/fix@11<!!a, !!b>::.ctor(class FSharpFunc`2<class FSharpFunc`2<!0, !1>, class FSharpFunc`2<!0, !1>>)
ldarg.1
tail.
call !!0 class FSharpFunc`2<class FSharpFunc`2<!!a, !!b>, !!a>::InvokeFast<!!b>(class FSharpFunc`2<!0, class FSharpFunc`2<!1, !!0>>, !0, !1)
ret
}
所以在我看来修复定义中的(fix f)
不是此时发生的递归调用,而只是对fix
本身的引用 - 以及参数{ {1}} - 被存储到名为f
的闭包中,并作为参数传递给lambda,然后通过此闭包实际调用Program/fix@11
。
否则这将是从一开始的无限递归,而fix
将是无用的。
我使用F#版本3.1.2,F#Interactive版本12.0.30815.0
我对替代解决方案不感兴趣。我只是想知道为什么编译器会返回一个fix
,当lambda没有产生结果时需要抛弃它。
答案 0 :(得分:7)
事实上,您已经回答了自己的问题。引用源代码中的注释,
// Throw away the unit result
是>强调后的待处理操作,阻止编译器在此处使用尾调用。
Keith Battocchi发表了一篇很棒的博文,"Tail calls in F#"(滚动到“限制/调用函数值返回单位”部分),发现了很多细节。
用两个词说:
通常,F#函数… -> unit
被编译为返回void
的.NET方法
但是,函数被视为值(例如,作为参数传递给高阶函数的函数)存储在('a->'b)
类型的对象中,因此它们实际返回Microsoft.FSharp.Core.Unit
,而不是{{ 1}}。
在返回之前,编译器需要将虚拟void
值弹出堆栈
因此,在递归调用之后 挂起操作,因此编译器无法将其优化为尾调用。
好消息:
请注意,只有在将第一类函数用作值时才会出现此问题。调用正常的.NET方法返回void 不会出现此问题,因为没有返回值从堆栈中弹出。
答案 1 :(得分:3)
要尾部调用优化代码,编译器必须调用优化fix
。在修复中使用高阶函数时,编译器会感到困惑。
如果你想要一个尾递归fix
,试着用不同的方式定义它:
let rec iter p f x =
if p x then x
else iter p f (f x)
iter ((=) 100000000) ((+) 1) 0
有趣的事实:由于Haskell如何计算表达式,你的fix
不会在Haskell中堆栈溢出:Haskell使用图缩减,这与使用调用堆栈不同。
let fix f = f (fix f)
fix (\f x -> if x == 100000000 then -1 else f (x + 1)) 0
说到你的第二个例子,.NET just-in-time可能能够在运行时优化尾调用。由于它是一个优化,它取决于运行时的智能程度:是否有返回值可能会使JIT优化器失效。 例如,我机器上的Mono并没有优化你的第二个例子。