English version is available at github

Yadriggy を使うと power assert を簡単に作ることができます。 なので作ってみました。

使い方

Power assert は assertion が失敗したとき、assertion の各部分式の 値を表示してくれる関数かと思います。 Groovy や .NET、JavaScript など様々なプログラミング言語で使え、もちろん Ruby でもすでに使えます。 既存の Ruby 実装はけっこう凝った実装ですが、Yadriggy を使えばわりに素直に実装できます。

Yadriggy 版の power assert も使い方は簡単です。

require 'yadriggy/assert'
arr = [2, 4, 6]
Yadriggy::Assert::assert { arr[1] % 2 != 0 }

Assertion は assert メソッドにブロックの形で渡します。 渡された assertion が失敗すると、assert メソッドは assertion 中の各部分式の実行結果を表示します。

--- Yadriggy::Assert ---
arr[1] % 2 != 0
|      | | |  |
|      | | |  0
|      | | false
|      | 2
|      0
4
------------------------

自分流の power assert を作る

自分流の power assert メソッドを定義すると便利なことがあります。 Yadriggy 版の assert メソッドの実装 は下のようになります。

def self.assert(&block)
  reason = Reason.new
  begin
    res = assertion(reason, block)
    puts_reason(reason) unless res
    return res
  rescue AssertFailure => evar
    puts_reason(evar.reason, evar)
    raise evar.cause
  end
end

このメソッドの主要部は assertion メソッドの呼び出しです。

res = assertion(reason, block)

assertion は assertion を実行して結果を返します。 最初の引数 reason は assertion の実行結果が記録されるオブジェクトです。 2番目の引数 block は assertion を含むブロックです。 これを使えば次のようなメソッドを自分で定義できます。

def my_assert(&block)
  reason = Yadriggy::Assert::Reason.new
  unless Yadriggy::Assert::assertion(reason, block)
    puts(reason.show)  # 実行結果を表示
    binding.pry        # pry で reason をさらに調査する
  end
end

reason.showString の配列を返します。 これは assertion の実行結果を表します。 reason.ast は assertion の抽象構文木です。 reason.results は hash 表で、部分式からそのソースコードおよび値を得るのに使います。

例えば reason.results[reason.ast] は、最初の要素が ast のソースコード、 2番目の要素が ast の実行結果である配列を返します。 同様に下のように各部分式の値を調べることができます。 部分式の値が大きなオブジェクトで、reason.show では大量のテキストが表示されてしまう場合に便利でしょう。

ast = reason.ast
results = reason.results
results[ast][1]           # assertion の値

# もし ast が + や < などの2項演算子式の場合
results[ast.left][1]      # 左辺の値
results[ast.right][1]     # 右辺の値

# もし ast が ! などの単項演算子式の場合
results[ast.operand][1]   # オペランドの値

# もし ast がメソッド呼び出しの場合
results[ast.receiver][1]  # レシーバの値
results[ast.args[0]][1]   # 第1引数の値

# もし ast が括弧式 (...) の場合
results[ast.expression][1]   # 式の値

実装

Yadriggy を使った実装は単純です。 ソースコードのプリプロセシングや専用の仮想機械は不要です。

まず assertion を含むブロックの抽象構文木を取り出します。

ast = Yadriggy::reify(block)

次にその抽象構文木をたどって、変数や直接解釈実行できない複雑な式のノードにあたったら、そのノードをソースコードに変換します。

src = Yadriggy::PrettyPrinter.ast_to_s(ast)

ここで ast は木のノード(葉あるいは中間ノード)です。 ast_to_s は抽象構文木に対応するソースコードを String オブジェクトの形で返します。 その後、eval でソースコードを実行します。

file_name, lineno = ast.source_location
eval(src, block.binding, file_name, lineno)

ast.source_locationast のソースコードのファイル名と行番号を返します。

これって DSL なの?

このような power assert の実装もドメイン専用言語 (DSL) と見なすことはできると思います。 Ruby のセマンティクスを少し拡張したセマンティクスの Ruby 風言語、というわけです。 セマンティクスはほぼ同じですが、部分式を実行する度に結果が Reason オブジェクトの中のログに記録される点が異なります。 こういうある種のログ記録システムは昔はアスペクト指向と呼ばれることもありましたが、DSL と考えてもよいのではないでしょうか。