我在使用 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 代码,可以将问题追溯到调用中的 lambda 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
底部的指令)。原因是编译器创建了对实际返回的 lambda 的调用Unit
!
所以用 C# 术语来说:Body 是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
}
现在有一个尾声。一切正常。
IL代码fix
(用于评论中的讨论):
.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 的定义内部并不是此时发生的递归调用,而只是对fix
自身的引用——连同参数f
——被存储到一个被调用的闭包Program/fix@11
中并传递给 lambda作为一个参数,然后fix
通过这个闭包实际调用。
否则,这将是从一开始就无限递归并且fix
毫无用处。
我正在使用 F# 版本 3.1.2,F# Interactive 版本 12.0.30815.0
请:
我对替代解决方案不感兴趣。我只想知道为什么Unit
当 lambda 不产生结果时编译器会返回需要丢弃的 a 。