htsign's blog

ノンジャンルで書くつもりだけど技術系が多いんじゃないかと思います

F# で構造的部分型のメソッドを呼び出す方法

これどっかで書いたような気がしてましたが、少なくともこのブログでは書いたことなかったようなので、すごく久しぶりに生存報告がてら書いてみようと思います。

F# は基本的に公称型で表現される言語なのですが、OCaml を直接の親に持つ言語だからなのか、構造的部分型で表現することも一応は可能です1
例えば、通常は以下のような書き方をします。

type Base() =
    abstract Echo : string -> unit
    default x.Echo text = printfn $"Base ({text})"

type Derived() =
    inherit Base()

    override x.Echo text = printfn $"Derived ({text})"

let echo (x : Base) (text : string) : unit =
    x.Echo text

let () =
    echo (Base()) "hello"    // ==> Base (hello)
    echo (Derived()) "hello" // ==> Derived (hello)

ここでは関数 echo が引数として Base ないしその派生型を要求しており、その Echo を呼び出しています。
BaseEcho が定義されているのだから、それを継承する Derived でも当然 Echo を呼び出すことができるため、正しくコンパイルができますね(C# とかと同じ)

では次の書き方です。

type TypeA() =
    member x.Echo (text : string) : unit = printfn $"TypeA ({text})"

type TypeB() =
    member x.Echo (text : string) : unit = printfn $"TypeB ({text})"

この TypeATypeB は全く別の型であり、何の継承関係もありません。しかし、全く同じシグネチャのメソッドを持っています。
C# にはこの2つの型を同列に扱う仕組みは(たぶん)ありません2。でも F# ならそれができます。この機能は SRTP と呼ばれます。

こう書きます。

type TypeA() =
    member x.Echo (text : string) : unit = printfn $"TypeA ({text})"

type TypeB() =
    member x.Echo (text : string) : unit = printfn $"TypeB ({text})"

let inline echo< ^a when ^a : (member Echo : string -> unit)> (x : ^a) (text : string) : unit =
    (^a : (member Echo : string -> unit) x, text)

let () =
    echo (TypeA()) "hello" // ==> TypeA (hello)
    echo (TypeB()) "hello" // ==> TypeB (hello)

この構造を制約として関数・メソッドの引数に取る方法は MSDN にしっかり書かれています
が、引数に取ったパラメータのメンバーを呼び出す方法はどこにも書かれていません3。なので当時4は調べるのがかなり骨でした。

なお、静的メンバーを制約にすることも可能で、その場合は以下のようになります。

type TypeA =
    static member Echo (text : string) : unit = printfn $"TypeA ({text})"

type TypeB =
    static member Echo (text : string) : unit = printfn $"TypeB ({text})"

let inline echo< ^a when ^a : (static member Echo : string -> unit)> (text : string) : unit =
    (^a : (static member Echo : string -> unit) text)

let () =
    echo<TypeA> "hello" // ==> TypeA (hello)
    echo<TypeB> "hello" // ==> TypeB (hello)

インスタンスメンバーの場合は第一引数にそのインスタンスを、静的メンバーの場合は単純に引数だけを渡してあげればよいです。

静的メンバーを制約としたシグネチャを扱えることで、例えば .NET でよく見る TryParse を統一的に扱うことができます。

let inline tryParseOpt< ^a when ^a : (static member TryParse : string * ^a byref -> bool)> (s : string) : ^a option =
    let mutable x = Unchecked.defaultof< ^a>

    if (^a : (static member TryParse : string * ^a byref -> bool) s, &x) then
        Some x
    else
        None

open System

let () =
    tryParseOpt<int> "123" |> Option.iter (printfn "%d")                            // ==> 123
    tryParseOpt<float> "123.456" |> Option.iter (printfn "%f")                      // ==> 123.456000
    tryParseOpt<DateTime> "05/05/2022 00:00:00+09:00" |> Option.iter (printfn "%A") // ==> 2022/05/05 00:00:00

Unchecked.defaultof は、対象とする型のゼロ値を得る関数ですね。例えば intfloat なら 0 を、配列や System.Uri など参照型なら null を取得します。
この関数は初期化コストが軽いので、とりあえず変数だけ先に宣言しておきたいときによく使います。

さて、ところで型制約と関数本体とに長ったらしい似たような記述を2回もするのダルいですよね。
実は型制約の方の記述は(多少の条件はあるものの)省略できます。

let inline tryParseOpt (s : string) : ^a option =
    let mutable x = Unchecked.defaultof< ^a>

    if (^a : (static member TryParse : string * ^a byref -> bool) s, &x) then
        Some x
    else
        None

こう書けるわけですね。だいぶシンプルになったんじゃないでしょうか。
ただし、こう書くことによって型引数を取れなくなります。

tryParseOpt "123" |> Option.iter (printfn "%d")
tryParseOpt "123.456" |> Option.iter (printfn "%f")

型引数が無くなりましたが、これらはこのままで正しくコンパイルできます。 "%d""%f" で渡されるべき型が明確なので、推論で型が一意に定まるからです。

tryParseOpt "05/05/2022 00:00:00+09:00" |> Option.iter (printfn "%A")

しかしこれはコンパイルエラーになります。理由は明白ですね。 "%A" では渡されるべき型があいまいだからです。
そんなときは型注釈の出番です。

(tryParseOpt "05/05/2022 00:00:00+09:00" : System.DateTime option) |> Option.iter (printfn "%A")

↑のように型を明示することによって正しくコンパイルが通るようになります。

型制約を記述すべきか省略すべきか、どちらがよいかはケースバイケースですが、個人的には付けてしまった方が後々困る場面が少なくなるかなと思います。


余談ですが、 OCaml では元々が構造的部分型なのでこういう記述は非常に簡単明快です。 echo の例を OCaml で書くとこんな感じです。

class type_a =
  object (self)
    method echo (text : string) : unit = Printf.printf "type_a (%s)\n" text
  end

class type_b =
  object (self)
    method echo (text : string) : unit = Printf.printf "type_b (%s)\n" text
  end

let echo (x : < echo : string -> unit; .. >) (text : string) =
  x#echo text

let () =
  echo (new type_a) "hello"; (* ==> type_a (hello) *)
  echo (new type_b) "hello"; (* ==> type_b (hello) *)

なお、 OCaml の型システム的には上記の例の type_atype_b は同じものなので、 < echo : string -> unit; .. > の部分を単に type_a に置き換えても動きます(紛らわしいので避けた方がいいですが)

通常 OCaml ではいちいち型は書かないので5

class type_a =
  object (self)
    method echo text = Printf.printf "type_a (%s)\n" text
  end

class type_b =
  object (self)
    method echo text = Printf.printf "type_b (%s)\n" text
  end

let echo x text =
  x#echo text

let () =
  echo (new type_a) "hello";
  echo (new type_b) "hello";

こんな感じですね。


余談の余談ですが、 < echo : string -> unit; .. > は「 echo : string -> unit と、他にもなんかメンバーを持ってるかもしれない何か」を表わす型です。
今回の例であれば < echo : string -> unit > でもOKですが、その場合は以下のような型を受け取れなくなります( echo 以外のメンバーを持つことが許されなくなるから)

class type_c =
  object (self)
    method echo (text : string) : unit = Printf.printf "type_c (%s)\n" text
    method echo_else : unit = print_endline "type_c (else)"
  end

  1. 公称的部分型や構造的部分型についてはこの記事が分かりやすい。

  2. リフレクションを使って強引に呼ぶことはできますが、受ける型は System.Object になってしまいますし、それゆえに実行時エラーの恐れもあるので非推奨です。

  3. 少なくとも私には見付けられませんでした。

  4. 3~5年くらい前の話

  5. 代わりに .mli ファイルにシグネチャを書きます。F# にも .fsi ファイルというものがありますが、通常手で書くことはないと思います。