LoginSignup
1
0

More than 3 years have passed since last update.

クラス定義の引数名にx使うのは避けよう

Last updated at Posted at 2020-03-05

問題が解決したので、記事のタイトルを変更。
「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。

intで試す
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になったと思われる。

  1. x(Gen型)のtoString()を呼び出す
  2. toString()でsprintf %A xが指定されたのでx(Gen型)のtoString()を呼び出す
  3. 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見てみたけど、ほしい情報は特に書いてなかった。

printfn

sprintf

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

1
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0