問題が解決したので、記事のタイトルを変更。
「stackoverflowの原因がわからない」→「クラス定義の引数名にx使うのは避けよう」
環境
dotnetcore:3.1.102
F# 4.7
事象
ジェネリック型を確かめてみようと思って試しに実装してみたんだけど
なぜかstackoverflowになった。
type Gen<'a>(x: 'a) =
override x.ToString() = sprintf "%A" x
Gen<int>(5) |> printfn "%A"
// => Stack overflow.
じゃあジェネリック型じゃなかったらどうなの?と思ってやってみたけど
やっぱりstackoverflow。
type Gen(x: int) =
override x.ToString() = sprintf "%A" x
Gen(5) |> printfn "%A"
対応
自己識別子をxから別のに変える。
type Gen(x: int) =
override this.ToString() = sprintf "%A" x
Gen(5) |> printfn "%A"
原因
F#では自己識別子は何でもよいのだが、慣習的にxがつけられているケースが多い。
たまたま変数名をxにして、自己識別子もxとしてしまったがためになかなかミスに気づけなかった。
プログラマとしては、xはコンストラクタで定義してるGenのint型メンバーのxだと思ってる。
コンパイラとしては、xは自己識別子なので、末尾のxもクラスオブジェクトそのものだと解釈する。
type Gen(x: int) =
//↓自己識別子なのでGen型 ↓自己識別子と同じxなのでGen型
override x.ToString() = sprintf "%A" x
sprintfやprintfnはフォーマットとして"%A"が指定されると、toString()を呼び出そうとする。
その結果、以下の無限ループが起きて、stackoverflowになったと思われる。
- x(Gen型)のtoString()を呼び出す
- toString()でsprintf %A xが指定されたのでx(Gen型)のtoString()を呼び出す
- 2の無限ループ
暗黙的コンストラクタに頼ってたから気づきにくかったけど
明示的にコンストラクタ書いたらどうなるんだ?と思って書いてみたら、ハッと気づいた。
type Gen =
val x:int
new(a) = {
x = a
}
// ionideがsprintfに渡しているxをintじゃなくてGen型と認識している
// もしかしてコンパイラが自己識別子のxと勘違いしてる?
override x.ToString() = sprintf "%A" x
Gen(5) |> printfn "%A"
// もちろんstack overflow.
type Gen =
val x:int
new(a) = {
x = a
}
// これでxがGen型ではなくintとして解釈される
override this.ToString() = sprintf "%A" this.x
Gen(5) |> printfn "%A"
// 5が出力される
別にsprintfとかprintfnの実装のせいではなかったw
type Gen<'a>(x: 'a) =
override this.ToString() = sprintf "%A" x
Gen<int>(5) |> printfn "%A"
// 5が出力される
教訓
クラスメソッド(コンストラクタ含む)の引数でx使うのは避けよう。
※もちろん自己識別子がx派でなければ避けなくてもいい
以下は古い記事(記録として残しておく)
仮説
たぶんsprintfかprintfnのどっちかの処理で無限再帰に
陥っているのではないかと思われるが原因はよくわからんまま。
ためしにToStringをオーバーライドせんかったらどうなるか?を見ると、こいつはエラーにならんかった。
組み合わせの問題?
type Gen<'a>(x: 'a) =
member x.print = printfn "%A" x
// override x.ToString() = sprintf "%A" x
Gen<int>(5) |> sprintf "%A" |> printfn "%A"
調査ログ
思いつく範囲でやってみたけど、頓挫。
原因をどうつきとめたらいいかわからないorz
MSDN見てみたけど、ほしい情報は特に書いてなかった。
Fsharp.Core.printf.fsかなとあたりをつけてコード追っかけてみたけど、今のF#力ではよくわからなかったw
最後のformatterの定義はどこにいるんやろ?
Githubの検索窓で検索かけても見つけられんかった。
[<CompiledName("PrintFormatLine")>]
let printfn format = fprintfn (!outWriter) format
[<CompiledName("PrintFormatLineToTextWriter")>]
let fprintfn (textWriter: TextWriter) format = kfprintf (fun _ -> textWriter.WriteLine()) textWriter format
[<CompiledName("PrintFormatToTextWriterThen")>]
let kfprintf continuation textWriter format =
doPrintf format (fun _ ->
TextWriterPrintfEnv(continuation, textWriter) :> PrintfEnv<_, _, _>
)
type TextWriterPrintfEnv<'Result>(k, tw : IO.TextWriter) =
inherit PrintfEnv<IO.TextWriter, unit, 'Result>(tw)
override __.Finish() : 'Result = k()
override __.Write(s : string) = tw.Write s
override __.WriteT(()) = ()
let inline doPrintf fmt f =
let formatter, n = Cache<_, _, _, _>.Get fmt
let env() = f(n)
// こいつどこにおるんや…
formatter env