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

# はじめに

カスタムオペレーションで何を書くかと思っていたのですが、bleisさんの[コンピュテーション式でキーワード引数](https://bleis-tift.hatenablog.com/entry/fsadvent2021)を見て、以前に考えていたものを共有しようと思った
実際に役立つかどうかはわからないが、型パズルをしていくのは楽しい

# 基本編

まずは、以下のパターンのコンピュテーション式の呼び出しを制御してみる
メソッドの呼び出し順序が違っていたり、呼び出されないメソッドがある場合はコンパイルエラーにさせたい

```fsharp
type CustomBuilder() =
  member _.Yield(_: unit) = ()
  member _.Run(_: unit) = ()

  [<CustomOperation("methodA")>]
  member _.MethodA(_: unit) = ()

  [<CustomOperation("methodB")>]
  member _.MethodB(_: unit) = ()

let builder = CustomBuilder()

// このパターン以外をコンパイルエラーにしたい
builder {
  methodA
  methodB
}
```

## 展開してみる

先ほどのコンピュテーション式を展開すると以下のようになる

```fsharp
builder.Yield(())
|> fun v -> builder.MethodA(v)
|> fun v -> builder.MethodB(v)
|> fun v -> builder.Run(v)
```

この展開結果をもとに、呼び出し順序を制御していく

### 開始を強制する

カスタムオペレーションを呼び出す場合、Yieldメソッドが呼ばれることを利用する
具体的には、判別共用体を使用して型の不一致が起こるようにする

```fsharp
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メソッドが呼び出されることを利用する

```fsharp
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に変更
```

### 順序を強制する

カスタムオペレーションの戻り値が、次のカスタムオペレーションの第一引数になること利用します

```fsharp
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に変更
```

## 基本編のまとめ

最初に想定したパターン以外をすべてコンパイルエラーにできると思う
また、判別共用体に値を持たせることもできる

```fsharp
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の値を設定
```

# 応用編

呼び出しても呼び出さなくてもいいメソッドを制御してみる
具体的には、以下のパターンのみを許容する

```fsharp
builder {
  methodA
  methodB
}

builder {
  methodA
  optMethod
  methodB
}
```

## 判別共用体を使ってみる

基本編のパターンを利用使用してみる

```fsharp
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を利用できる

```fsharp
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パターン

どちらか一方が必ず呼ばれるパターン

```fsharp
builder {
  methodA
  altMethodA
  methodB
}

builder {
  methodA
  altMethodB
  methodB
}
```

```fsharp
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回以上のループを行うパターン

```fsharp
builder {
  methodA
  methodB
}

builder {
  methodA
  loopMethod
  methodB
}

builder {
  methodA
  loopMethod
  loopMethod
  methodB
}
```

```fsharp
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回以上のループを行うパターン

```fsharp
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だとさすがに規模が大きそうなので、どこかに手頃な構文の物がないだろうか