コンピュテーション式の Quote メソッドで変換結果を見る

前提

以下のエントリを読んでいることが前提となります。

詳説コンピュテーション式

また、F# のコードクォートに関する知識を持っていると、理解しやすいと思います。

今回のお話

これに関することをメモ書きしておきたかった。

Quote メソッド

Run, Delay, Quote メソッドが定義されていると、コンピュテーション式は以下のように変換されます。

let b = builder-expr in b.Run (<@ b.Delay(fun () -> {| cexpr |}C) >@)

Quote メソッドはコードクォートに変換されることがわかります。

この Quote を使って何か面白いことができないか…と考えるのですが、既存のコンピュテーション式の多くは既にコードクォートを受け取らない Run を実装しています。単に Quote だけを拡張メソッドとして定義するだけでは、コンピュテーション式を利用した既存コードがコンパイルエラーになってしまいます。

まずはこの問題を解決しましょう。

拡張メソッドで呼び出すメソッドを制御する

コンピュテーション式はクラスなので、拡張メソッドやオーバーロードが利用できます。この2つを使うことで、コードクォートを引数にとらない Run が既に実装されていても、コンパイルエラーを回避できます。

例として、コンピュテーション式の実装にStateを用いる で定義した OptionBuilder を使います。定義を再掲します。

module Program

type OptionBuilder() =
  member this.Return(x) = Some x
  member this.ReturnFrom(m) = m
  member this.Bind(m, f) = Option.bind f m
  member this.Zero() = None
  member this.Combine(m, f) = Option.bind f m
  member this.Delay(f) = f
  member this.Run(f) = f ()

let option = OptionBuilder()

では、拡張してみましょう。

module Ext =

  type OptionBuilder with
    member this.Quote() = ()
    // コードクォートを受け取ってそのまま返す
    member this.Run(f: Quotations.Expr<unit -> 'a option>) = f

  // Expr
  let test<'a> = option {
      return 0
      return 1
    }

testprintfn "%A" に渡すと、以下の値が表示されると思います。

Call (Some (Value (Program+OptionBuilder)), Delay,
      [Lambda (unitVar,
               Call (Some (Value (Program+OptionBuilder)), Combine,
                     [Call (Some (Value (Program+OptionBuilder)), Return,
                            [Value (0)]),
                      Call (Some (Value (Program+OptionBuilder)), Delay,
                            [Lambda (unitVar,
                                     Call (Some (Value (Program+OptionBuilder)),
                                           Return, [Value (1)]))])]))])

この結果を大雑把に説明すると、

  1. ラムダ式を引数に Delay が呼び出される
  2. ラムダ式の引数は unit で、ラムダ式内では Combine が呼ばれる
  3. Combine の第1引数は Return に 0 を渡した結果、第2引数は 式を Delay でくるんだものが入る

のような感じです。コンピュテーション式の変換結果がそれとなくわかりますね。定義したコンピュテーション式が意図した通りに展開されているかを簡易的に眺めたいときに便利かもしれません。

ちなみに、この方法は拡張メソッドとオーバーロードを利用するだけなので、標準ライブラリのコンピュテーション式も制御できます。

  type AsyncBuilder with
    member this.Quote() = ()
    member this.Run(f: Quotations.Expr<_>) = f  

  // 型はExpr<unit>
  let asyncTest = async {
    return! Async.Sleep 1000
  }

Quote メソッドの実装に関して

Quote メソッドが存在する場合は、 Delay を引用符で囲んだ形で出力されます。このことを考えると、最小の実装は以下の通りです。

type OptionBuilder with
    member this.Quote() = ()

引数を渡すパターンだと

type OptionBuilder with
    member this.Quote(q: Quotations.Expr<_>) = q

となることが多いでしょう。

コードクォートを使ってコンピュテーション式を制御する

Quotations.Expr を引数として受け取り、Expr の評価結果を返すような eval 関数を実装し、 Run の引数の Expr を eval に適用すると、通常のコンピュテーション式の計算結果と同様の結果を得られます。

この eval の中で、特定のメソッドが Call されていたら以降の計算を破棄する、という実装を行えば、この記事で取り上げたような State を併用せずとも、フローを変更できるでしょう *1 。Quotations.Patterns, Quotations.DerivedPatterns, Quotations.ExprShape で定義されている各種アクティブパターンを使えば、式木を解析することができます。

式木を実際に評価する方法については、残念ながら私にはリフレクションを使うことしか思いつきませんでした。でもそうなると、パフォーマンス劣化を招く恐れがあるので、この方法を使うかどうかはケースバイケースですね。

最後に

eval をどう実装するかについては、Quotations の話になってくるので省略します。興味のある方は色々と試してみてください。

*1:つまりこの方法が、Return 以降の処理を破棄する解決案3ということになります