2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

F#Advent Calendar 2021

Day 24

カスタムオペレーションの呼び出し順序を制御する

Posted at

はじめに

カスタムオペレーションで何を書くかと思っていたのですが、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だとさすがに規模が大きそうなので、どこかに手頃な構文の物がないだろうか

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?