Quoteメソッドでコンピュテーション式に介入する(実践編?)

全国コンピュテーション式ユーザの皆様こんにちは。

GWの進捗どうですか?

私は駄目です。 ネプテューヌVⅡを1周しようと思っていたのにまだできていません…。

さて、今回はQuoteメソッドを使ってコンピュテーション式への介入しようという話の実践編(?)です。 疑問符つきなのは、プロトタイプしかできてないからという話ですね。

題材は例によってPersimmonです。

読んでおいたほうが良い資料

詳説コンピュテーション式 - ぐるぐる~ コンピュテーション式の変形後を覗き見る #FsAdvent - 眠気と戦う日々 コンピュテーション式の変形後を覗き見るを改良する - ぐるぐる~

コード全貌

persimmon-projects/Persimmon.Pudding at f3789f7cf6682bcdcab0a8ae1a90532bdcdda4fe · GitHub

これを見ればなんとなくわかると思います。

実装の話

というわけでここから本編です。

前提とゴール

今回のゴールは次のようになります。

元のテストコードにモジュールの open を加えるだけで、テスト失敗時にパラメータと簡約結果の一部が出力されるようになる

また、前提条件として

  • Persimmonには一切手を入れない

があります。 まぁ、手を入れないからこその拡張ライブラリですよね、的な。

下ごしらえ

Quoteメソッドを追加することで得られる Expr<'T> を自前で解析するのは手間なので、FSharp.Quotations.Evaluatorを fork して改造するため、同じリポジトリに同梱したりライブラリ名が衝突しないように名前を変えます。

また、 FSharp.Quotations.Evaluatorから Compile 以外の public な関数、メソッドを削除します。 他のことは一切できないよということを明示したいとかなんとか。

なぜFSharp.Quotations.Compilerを使わなかったの?

面倒くさかったLetRecursiveがまだ未実装だったからです。 あと、今回はスタックオーバーフローは気にしなくてもいいだろうという慢心もあります。

ただ、パフォーマンス調査次第では乗り換える可能性も残っています。

引数、戻り値をDictionaryに登録する

このあたりの関数を呼び出すことで、Expr<'T> が eval されたときに Dictionary に値が登録されるようになります。

λ式の引数は戻り値が unit な登録関数を手続き的にλ式のbodyにくっつければよいだけです。

値の適用や戻り値の結果を登録するには、Dictionaryに登録したあとで元の値を返す必要があります。

仕上げとして、 こんな感じ でDictionaryと Expr<'T>コンパイル結果を返します。 これで、 eval 後に 実行中の変数や簡約結果を取得できるようになります。

コンピュテーション式を拡張する

QuoteとQuote用のRunメソッドを型拡張で追加します。 これで、モジュールをopenすれば Quote が存在することをトリガーに `Expr TestCase<'T>> を引数にとる Run メソッドを呼び出すように変換が行われます。 あとは、Dictionaryから情報を引っ張ってきてこねくり回すだけです。

今回はDelayが邪魔だったので除去してからEvaluatorに渡していますが、Delayが仮に何らかの処理を行っている場合は除去するべきではないでしょう。

実行結果

open Persimmon
open Persimmon.Pudding.Quotations // 既存のテストコードにこれを追加するだけ
open UseTestNameByReflection

let ``return int`` = test {
  return 1
}

let ``fail test`` = test {
  let! a = ``return int``
  do! assertEquals 2 a
  return a
}

このようなテストを実行すると

.x
Assertion Violated: fail test
1. [parameter]
     _arg1: System.Int32 -> 1
     _arg2: Microsoft.FSharp.Core.Unit -> <null>
     a: System.Int32 -> 1
   [method call]
     Persimmon.TestBuilder.Return(1) -> TestCase<Int32>({Name = "";
    Parameters = [];})
     assertEquals(2, 1) -> NotPassed (Violated "Expect: 2
   Actual: 1")

2. Expect: 2
   Actual: 1
============================== summary ===============================
run: 2, error: 0, violated: 1, skipped: 0, duration: 00:00:00.3012002

なんとなくパラメータと簡約結果の一部が取得できているのがわかると思います。

_arg1let! aのBindの変換で現れるλ式の引数、_arg2do! の Bindの変換で現れるλ式の引数です。 きちんと実装すればこの辺りを除去できますが、まぁそれは今後の課題ということで…。

まとめ

Quoteメソッドを追加し、式木をいじることでコンピュテーション式に介入することができました。

これをがんばっていくと"コンピュテーション式内の途中経過をいい感じにdump"できるんじゃないかなと思っています。 そういう拡張ライブラリがほしかったので、今回のこれは Persimmon.Pudding として育ててみようと思っています。