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
を呼び出しています。
Base
に Echo
が定義されているのだから、それを継承する 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})"
この TypeA
と TypeB
は全く別の型であり、何の継承関係もありません。しかし、全く同じシグネチャのメソッドを持っています。
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
は、対象とする型のゼロ値を得る関数ですね。例えば int
や float
なら 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_a
と type_b
は同じものなので、 < echo : string -> unit; .. >
の部分を単に type_a
に置き換えても動きます(紛らわしいので避けた方がいいですが)
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