はじめに
カスタムオペレーションで何を書くかと思っていたのですが、bleisさんのコンピュテーション式でキーワード引数を見て、以前に考えていたものを共有しようと思った
実際に役立つかどうかはわからないが、型パズルをしていくのは楽しい
基本編
まずは、以下のパターンのコンピュテーション式の呼び出しを制御してみる
メソッドの呼び出し順序が違っていたり、呼び出されないメソッドがある場合はコンパイルエラーにさせたい
type CustomBuilder() =
member _.Yield(_: unit) = ()
member _.Run(_: unit) = ()
[<CustomOperation("methodA")>]
member _.MethodA(_: unit) = ()
[<CustomOperation("methodB")>]
member _.MethodB(_: unit) = ()
let builder = CustomBuilder()
// このパターン以外をコンパイルエラーにしたい
builder {
methodA
methodB
}
展開してみる
先ほどのコンピュテーション式を展開すると以下のようになる
builder.Yield(())
|> fun v -> builder.MethodA(v)
|> fun v -> builder.MethodB(v)
|> fun v -> builder.Run(v)
この展開結果をもとに、呼び出し順序を制御していく
開始を強制する
カスタムオペレーションを呼び出す場合、Yieldメソッドが呼ばれることを利用する
具体的には、判別共用体を使用して型の不一致が起こるようにする
type NextMethodA = NextMethodA // 次はMethodAを呼び出すことを示す型
type CustomBuilder() =
member _.Yield(_: unit) = NextMethodA // 戻り値の型をNextMethodAに変更
member _.Run(_: unit) = ()
[<CustomOperation("methodA")>]
member _.MethodA(_: NextMethodA) = () // 引数の型をNextMethodAに変更
[<CustomOperation("methodB")>]
member _.MethodB(_: unit) = ()
終了を強制する
コンピュテーション式が終了する場合、Runメソッドが呼び出されることを利用する
type NextMethodA = NextMethodA
type EndExpr = EndExpr // 式が終了することを示す型
type CustomBuilder() =
member _.Yield(_: unit) = NextMethodA
member _.Run(_: EndExpr) = () // 引数の型をEndExprに変更
[<CustomOperation("methodA")>]
member _.MethodA(_: NextMethodA) = ()
[<CustomOperation("methodB")>]
member _.MethodB(_: unit) = EndExpr // 戻り値の型をEndExprに変更
順序を強制する
カスタムオペレーションの戻り値が、次のカスタムオペレーションの第一引数になること利用します
type NextMethodA = NextMethodA
type NextMethodB = NextMethodB // 次はMethodBを呼び出すことを示す型
type EndExpr = EndExpr
type CustomBuilder() =
member _.Yield(_: unit) = NextMethodA
member _.Run(_: EndExpr) = ()
[<CustomOperation("methodA")>]
member _.MethodA(_: NextMethodA) = NextMethodB
[<CustomOperation("methodB")>]
member _.MethodB(_: NextMethodB) = EndExpr // 引数の型をNextMethodBに変更
基本編のまとめ
最初に想定したパターン以外をすべてコンパイルエラーにできると思う
また、判別共用体に値を持たせることもできる
type User = { Name: string; Password: string }
type NextName = NextName
type NextPassword = NextPassword of string // nameの値を保持できる
type EndUser = EndUser of (string * string) // nameとpasswordを保持できる
type UserBuilder() =
member _.Yield(_: unit) = NextName
member _.Run(EndUser (name, password)): User = { Name = name; Password = password }
[<CustomOperation("name")>]
member _.MethodA(_: NextName, name) = NextPassword name // nameの値を設定
[<CustomOperation("password")>]
member _.MethodB(NextPassword name, password) = EndUser (name, password) // nameとpasswordの値を設定
応用編
呼び出しても呼び出さなくてもいいメソッドを制御してみる
具体的には、以下のパターンのみを許容する
builder {
methodA
methodB
}
builder {
methodA
optMethod
methodB
}
判別共用体を使ってみる
基本編のパターンを利用使用してみる
type NextMethodA = NextMethodA
type NextOptMethodOrMethodB = NextOptMethodOrMethodB
type NextMethodB = NextMethodB
type EndExpr = EndExpr
type CustomBuilder() =
member _.Yield(_: unit) = NextMethodA
member _.Run(_: EndExpr) = ()
[<CustomOperation("methodA")>]
member _.MethodA(_: NextMethodA) = NextOptMethodOrMethodB
[<CustomOperation("optMethod")>]
member _.OptMethod(_: NextOptMethodOrMethodB) = NextMethodB
[<CustomOperation("methodB")>]
member _.MethodB(_: NextOptMethodOrMethodB) = EndExpr
let builder = CustomBuilder()
builder {
methodA
methodB
}
builder {
methodA
optMethod // ここでコンパイルエラー
methodB
}
NextOptMethodOrMethodBとNextMethodBで互換性がないため、コンパイルエラーになる
interfaceを利用する
互換性がないなら作ればいい、ということでinterfaceを利用する
幸い、判別共用体はinterfaceを利用できる
type INextOptMethod = interface end
type INextMethodB = interface end
type NextMethodA = NextMethodA
type NextOptMethodOrMethodB = NextOptMethodOrMethodB with
interface INextOptMethod
interface INextMethodB
type NextMethodB = NextMethodB with
interface INextMethodB
type EndExpr = EndExpr
type CustomBuilder() =
member _.Yield(_: unit) = NextMethodA
member _.Run(_: EndExpr) = ()
[<CustomOperation("methodA")>]
member _.MethodA(_: NextMethodA) = NextOptMethodOrMethodB
[<CustomOperation("optMehtod")>]
member _.OptMethod(_: INextOptMethod) = NextMethodB
[<CustomOperation("methodB")>]
member _.MethodB(_: INextMethodB) = EndExpr
その他の応用
altパターン
どちらか一方が必ず呼ばれるパターン
builder {
methodA
altMethodA
methodB
}
builder {
methodA
altMethodB
methodB
}
type INextAltMethodA = interface end
type INextAltMethodB = interface end
type NextMethodA = NextMethodA
type NextAltMethod = NextAltMethod with
interface INextAltMethodA
interface INextAltMethodB
type NextMethodB = NextMethodB
type EndExpr = EndExpr
type CustomBuilder() =
member _.Yield(_: unit) = NextMethodA
member _.Run(_: EndExpr) = ()
[<CustomOperation("methodA")>]
member _.MethodA(_: NextMethodA) = NextAltMethod
[<CustomOperation("altMehtodA")>]
member _.AltMethodA(_: INextAltMethodA) = NextMethodB
[<CustomOperation("altMehtodB")>]
member _.AltMethodB(_: INextAltMethodB) = NextMethodB
[<CustomOperation("methodB")>]
member _.MethodB(_: NextMethodB) = EndExpr
loopパターン
0回以上のループを行うパターン
builder {
methodA
methodB
}
builder {
methodA
loopMethod
methodB
}
builder {
methodA
loopMethod
loopMethod
methodB
}
type INextLoopMethod = interface end
type INextMethodB = interface end
type NextMethodA = NextMethodA
type NextLoopMethodOrMethodB = NextLoopMethodOrMethodB with
interface INextLoopMethod
interface INextMethodB
type NextMethodB = NextMethodB
type EndExpr = EndExpr
type CustomBuilder() =
member _.Yield(_: unit) = NextMethodA
member _.Run(_: EndExpr) = ()
[<CustomOperation("methodA")>]
member _.MethodA(_: NextMethodA) = NextLoopMethodOrMethodB
[<CustomOperation("loopMethod")>]
member _.LoopMethod(_: INextLoopMethod) = NextLoopMethodOrMethodB
[<CustomOperation("methodB")>]
member _.MethodB(_: INextMethodB) = EndExpr
1回以上のループを行うパターン
type INextLoopMethod = interface end
type INextMethodB = interface end
type NextMethodA = NextMethodA
type NextLoopMethod = NextLoopMethod with
interface INextLoopMethod
type NextLoopMethodOrMethodB = NextLoopMethodOrMethodB with
interface INextLoopMethod
interface INextMethodB
type NextMethodB = NextMethodB
type EndExpr = EndExpr
type CustomBuilder() =
member _.Yield(_: unit) = NextMethodA
member _.Run(_: EndExpr) = ()
[<CustomOperation("methodA")>]
member _.MethodA(_: NextMethodA) = NextLoopMethod // 変更点
[<CustomOperation("loopMethod")>]
member _.LoopMethod(_: INextLoopMethod) = NextLoopMethodOrMethodB
[<CustomOperation("methodB")>]
member _.MethodB(_: INextMethodB) = EndExpr
応用編のまとめ
基本的には状態遷移図で記載できるものは制御できると思う
カスタムオペレーションの引数がインターフェース、戻り値は判別共用体に統一したらよさそう
interfaceを経由すれば、値を保持することもできる(かなり不便なので、代替案が欲しいところ
最後に
ここまで制御が効くのであれば、SQLのようなものも記述できるのではないかと思った
SQLだとさすがに規模が大きそうなので、どこかに手頃な構文の物がないだろうか