Power assert を Ruby 用に自作する
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.show
は String
の配列を返します。
これは 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_location
は ast
のソースコードのファイル名と行番号を返します。
これって DSL なの?
このような power assert の実装もドメイン専用言語 (DSL) と見なすことはできると思います。
Ruby のセマンティクスを少し拡張したセマンティクスの Ruby 風言語、というわけです。
セマンティクスはほぼ同じですが、部分式を実行する度に結果が Reason
オブジェクトの中のログに記録される点が異なります。
こういうある種のログ記録システムは昔はアスペクト指向と呼ばれることもありましたが、DSL と考えてもよいのではないでしょうか。