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
!を返すラムダへの呼び出しを作成したためです。
したがって、C# の用語で言えば、 Body はFunc<Int32,Unit>
実際にあるべきときAction<Int32>
です。呼び出しは破棄する必要があるものを返すため、末尾呼び出しにすることはできません。また、メソッドはではなくf@1
としてコンパイルされることに注意してください。これが、引数の呼び出しの結果を破棄する必要がある理由です。void
Unit
これは実際に意図されたものですか、それとも何かできることはありますか? コンパイラがこのラムダを処理する方法により、固定小数点コンビネータは、私が意図したすべての目的で役に立たなくなります。
結果として何かを返す限り、それは正常に機能することを付け加えたいだけです。何も返さない関数だけが期待どおりに機能しません。
これは機能します:
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
これは、ラムダ用に生成されたコードです。
.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
、ラムダに渡されるように見えます。fix
このクロージャーを介して実際に呼び出す引数として。
そうしないと、最初から無限再帰にfix
なり、役に立たなくなります。
F# バージョン 3.1.2、F# Interactive バージョン 12.0.30815.0 を使用しています。
お願いします:
代替ソリューションには興味がありません。Unit
ラムダが結果を生成しない場合に、破棄する必要がある a をコンパイラが返す理由を知りたいだけです。