コンピュテーション式でキーワード引数
これはF# Advent Calendar 2021の23日分の記事です。
この間の第3回 FUN FAN F#に参加したところ、カスタムオペレーターをキーワード引数のように使っていたのをみて思いついたネタを紹介します。 あくまでネタですので、実際にこのようなAPIを提供するのかどうかの判断は各自に任せます。
カスタムオペレーターの説明は、カスタムオペレーションハンズオンなどを参考にしていただくとして、今回目指すのは以下のようなAPIです。
let str = "F# Advent Calendar 2021" let res1 = str |> substring { start 3; end_ 9 } let res2 = str |> substring { start 19; length 4 } printfn "%s, %s" res1 res2 // => Advent, 2021
これだけだとつまらないので、次のようなものはコンパイルエラーにしたいです。
// end_とlengthは両方指定できないようにしたい substring { start 3; end_ 9; length: 10 } // startがないとエラーにしたい substring { end_ 9 } // 同じキーワードを複数指定してほしくない substring { start 3; end_ 9; start: 4 }
また、順不同にしたいですよね。
// 次の2つは同じ substring { start 3; end_ 9 } substring { end_ 9; start 3 }
考え方
コンピュテーション式では、Run
を提供することでコンピュテーション式で組み立てた結果を Run
の引数に渡せるようになります。
ということで、Run
をオーバーロードして型によって処理を切り分けるということができます。
そして、カスタムオペレーションで許される組み合わせが型で指定できれば、それ以外の組み合わせはエラーにできます。 この方針で作っていきます。
作り方
まずは、設定されていないキーワードを表す型を用意します。
type NotSet = NotSet
次に、カスタムオペレーションを使うので、ビルダークラスに Yield
が必要です。
member _.Yield (()) = (NotSet, NotSet, NotSet)
Yield
はタプルを返すようになっていて、前から順に start
, end_
, length
に対応します*1。
当然、初期段階ではどのキーワードも指定されていないため、すべて NotSet
です。
次に、カスタムオペレーションごとに対応するタプルの要素を更新します。 例えばこんな感じです。
[<CustomOperation("end_")>] member _.End((startParam: 'a, _: NotSet, lengthParam: NotSet), end_: int) = (startParam, end_, lengthParam)
タプルの最初の要素(startParam
) は end_
を呼び出す前でも後でも設定されてくれればいいので、なんでも受け取れるようにしています。
タプルの2番目の要素は、end_
自体ですのでこれは End
の第2引数を使うため、使いません。
なので、_
で受けています。
また、型を NotSet
に限定していますが、これを限定せずに 'b
とすることで、何度も設定できてかつ後勝ちになるようなAPIを提供できます。
タプルの最後の要素(lengthParam
)は NotSet
に限定しています。
これは、end_
オペレーターを呼び出す前に length
オペレーターを呼び出していた場合、コンパイルエラーにしたいからです。
このように、
- このオペレーターが呼び出された場合にエラーにしたいキーワードは
NotSet
を指定 - このオペレーターが呼び出された場合にエラーにしたくないキーワードは型パラメーターを指定
することで、変な組み合わせを弾けるようにしています。
最後に、適切な組み合わせの場合に適切な処理をする Run
を定義して完成です*2。
member _.Run((start: int, end_: int, _: NotSet)) = fun (str: string) -> str.Substring(start, end_ - start) member _.Run((start: int, _: NotSet, length: int)) = fun (str: string) -> str.Substring(start, length)
全体
全体はこんな感じです。
type NotSet = NotSet type SubstringBuilder () = member _.Yield (()) = (NotSet, NotSet, NotSet) [<CustomOperation("start")>] member _.Start ((_: NotSet, endParam: 'b, lengthParam: 'c), start: int) = (start, endParam, lengthParam) [<CustomOperation("end_")>] member _.End ((startParam: 'a, _: NotSet, lengthParam: NotSet), end_: int) = (startParam, end_, lengthParam) [<CustomOperation("length")>] member _.Length ((startParam: 'a, endParam: NotSet, _: NotSet), length: int) = (startParam, endParam, length) member _.Run((start: int, end_: int, _: NotSet)) = fun (str: string) -> str.Substring(start, end_ - start) member _.Run((start: int, _: NotSet, length: int)) = fun (str: string) -> str.Substring(start, length) let substring = SubstringBuilder ()
ネタばらし?
お気づきの方もいるかもしれませんが、これは型安全なBuilderパターンをコンピュテーション式で再解釈したものですね。 型制約とか使わなくていいので、オリジナルよりも大分シンプルに実装できているような気はします。
余談
今日で35歳になってしまいました。 プログラマの定年なので、年金ください。