コンピュテーション式でキーワード引数 その2
コンピュテーション式でキーワード引数を改良しました。 前回の記事では、以下のような欠点がありました。
- 実行効率が悪い
- エラーメッセージが分かりにくい
今回はこの2つの欠点を解決してみましょう。
実行効率をよくする
ビルダーのメソッドに inline
指定をすればいい感じになります。
このようなコードを考えてみます。
let f () = "hoge hoge" |> substring { start 5; end_ 7 }
このコードを inline
を付けずにコンパイルしたILはこのようになります。
IL_0000: call class Program/SubstringBuilder Program::get_substring() IL_0005: ldc.i4.5 IL_0006: ldc.i4.7 IL_0007: call class Program/NotSet Program/NotSet::get_NotSet() IL_000c: newobj instance void class System.Tuple`3<int32,int32,class Program/NotSet>::.ctor(!0, !1, !2) IL_0011: callvirt instance class Microsoft.FSharp.Core.FSharpFunc`2<string,string> Program/SubstringBuilder::Run(class System.Tuple`3<int32,int32,class Program/NotSet>) IL_0016: ldstr "hoge hoge" IL_001b: tail. IL_001d: callvirt instance !1 class Microsoft.FSharp.Core.FSharpFunc`2<string,string>::Invoke(!0) IL_0022: ret
F#コードにするとこんな感じでしょうか。
let f () = "hoge hoge" |> substring.Run((5, 7, NotSet))
タプルを構築したり、Run
で返ってきた関数を呼び出していることがわかります。
それに対して、inline
を付けてコンパイルするとこのようになります。
IL_0000: ldstr "hoge hoge" IL_0005: ldc.i4.5 IL_0006: ldc.i4.2 IL_0007: callvirt instance string System.String::Substring(int32, int32) IL_000c: ret
これをF#コードにするとこんな感じです。
let f () = "hoge hoge".Substring(5, 2)
すべて展開され、単なる Substring
呼び出しにできています*1。
エラーメッセージをわかりやすくする
このようなコードを考えてみます。
let f () = "hoge hoge" |> substring { start 5; end_ 7; length 2 }
ここで、end_ 7
の部分がエラーになり、エラーメッセージはこのようになります。
error FS0193: 型の制約が一致しません。次の型 'int * int * NotSet' は次の型と互換性がありません 'int * NotSet * NotSet'
これは、元のコードが意味的には次のコードと同じになるためです。
let b = substring "hogehoge" |> b.Run(b.Length(b.End(b.Start(b.Yield(()), 5), 7), 2))
End
の戻り値の型は 'a * int * NotSet
であり、Length
の引数の型は 'a * NotSet * NotSet
なので、
このようなエラーメッセージになるのです*2。
丁寧に読み解けばわからなくはないメッセージですが、わかりやすいとは言えません。
これを解決する一つの方法としては、NotSet
の部分にメッセージを埋め込む方法が考えられますが、ちょっと無理矢理感が高いです。
F#6.0からカスタムオペレーターのメソッドがオーバーロードできるようになったので、
これと CompilerMessage
属性を組み合わせるといい感じにコンパイルエラーメッセージが作れます。
[<CustomOperation("length")>] [<CompilerMessage("lengthはend_と一緒には使えません", 10001, IsError=true)>] member _.Length ((_: 'a, _: int, _: NotSet), _: int) : 'a * int * NotSet = failwith "oops!"
Length
メソッド側にオーバーロードを追加し、end_
が設定されている、
つまりは第一引数のタプルの第2要素が int
になっているオーバーロードが選択された場合に、
コンパイルエラーにしてしまうのです。
そのため、length 2
の部分がエラーになり、エラーメッセージはこのようになります。
error FS10001: lengthはend_と一緒には使えません
以前は合致するメソッドが見つからなかったため、End
側がエラーになっていましたが、
今回は合致するオーバーロードが見つかったうえでコンパイルエラーになるため、
エラーの場所もより自然な場所に出るようになっています。
また、戻り値の型を指定しています。
戻り値の方を指定しない場合は戻り値の型がジェネリックになります。
Run
はオーバーロードされており、戻り値の型がジェネリックだと適切な Run
が選べません。
そのため、substring
の部分にもコンパイルエラーが出てしまいます。
戻り値の型指定は、これを避けるために付けています*3。
全体
今回のコードの全体はこのようになります。
type NotSet = NotSet type SubstringBuilder () = member inline _.Yield (()) = (NotSet, NotSet, NotSet) [<CustomOperation("start")>] member inline _.Start ((_: NotSet, endParam: 'b, lengthParam: 'c), start: int) = (start, endParam, lengthParam) [<CustomOperation("start")>] [<CompilerMessage("startは2回指定できません", 10001, IsError=true)>] member _.Start ((_: int, _: 'b, _: 'c), _: int) : int * 'b * 'c = failwith "oops!" [<CustomOperation("end_")>] member inline _.End ((startParam: 'a, _: NotSet, lengthParam: NotSet), end_: int) = (startParam, end_, lengthParam) [<CustomOperation("end_")>] [<CompilerMessage("end_は2回指定できません", 10001, IsError=true)>] member _.End ((_: 'a, _: int, _: 'b), _: int) : 'a * int * 'b = failwith "oops!" [<CustomOperation("end_")>] [<CompilerMessage("end_はlengthと一緒には使えません", 10001, IsError=true)>] member _.End ((_: 'a, _: NotSet, _: int), _: int) : 'a * NotSet * int = failwith "oops!" [<CustomOperation("length")>] member inline _.Length ((startParam: 'a, endParam: NotSet, _: NotSet), length: int) = (startParam, endParam, length) [<CustomOperation("length")>] [<CompilerMessage("lengthは2回指定できません", 10001, IsError=true)>] member _.Length ((_: 'a, _: 'b, _: int), _: int) : 'a * 'b * int = failwith "oops!" [<CustomOperation("length")>] [<CompilerMessage("lengthはend_と一緒には使えません", 10001, IsError=true)>] member _.Length ((_: 'a, _: int, _: NotSet), _: int) : 'a * int * NotSet = failwith "oops!" member inline _.Run((start: int, end_: int, _: NotSet)) = fun (str: string) -> str.Substring(start, end_ - start) member inline _.Run((start: int, _: NotSet, length: int)) = fun (str: string) -> str.Substring(start, length) let substring = SubstringBuilder ()