ScalaっぽいパターンマッチをRubyで実装するから始まった 一連のpattern-matchライブラリ関連エントリのおそらく最終章。 任意のオブジェクトに対して正規表現相当のマッチをできるようにしてみたのでその紹介です。
どういうことかと言うと、例えば配列をFixnumの前後で分割したりとか
match([:a, 0, :b, :c]) do
with(_[*a, Fixnum, *b]) do
a #=> [:a]
b #=> [:b, :c]
end
end
連続した要素の積が12になるところを探したりとか
match([1, 2, 3, 4, 5]) do
with(_[*_, *a, *_], guard { a.inject(:*) == 12 }) do
a #=> [3, 4]
end
end
「シーケンス中で連続して同じ値が入っている各箇所について,2 個目以降は削除したシーケンス」を取得したり (前後の値も利用したシーケンス処理 - NyaRuRuが地球にいたころ)とか
def replace_repeated(obj, &block)
ret = match(obj, &block)
if ret == obj
ret
else
replace_repeated(ret, &block)
end
rescue PatternMatch::NoMatchingPatternError
obj
end
replace_repeated([1, 2, 4, 4, 3, 3, 4, 0, 0]) do
with(_[*a, x, x, *b]) { [*a, x, *b] }
end #=> [1, 2, 4, 3, 4, 0]
こういった処理が簡単に書けるようになりました。
先日のRuby開発者会議でパターンマッチを入れたいねみたいな話をしてきたのですが、 せっかくならこのぐらい出来るようになると楽しそうです。
フィルタコマンドを作るときなどに Better Errors の様な感じで使えるREPLが欲しくなって、 ライブラリを作ってみたことがあります(Rubyの例外終了時に自動でREPLを起動する)。 *1
その時は、set_trace_funcのオーバーヘッドを避けるためにStandardError#initializeをCレベルで定義して その中でbindingを取得する*2という ややトリッキーな実装をしていたのですが、Ruby 2.0でset_trace_funcの高速版といえるTracePointが導入されたのでそちらを使って書き直してみました。
主な変更点は2点。
gem installしてrequireするだけで利用できます(当然、1.9では動きません)。
$ gem install dexc
$ cat t.rb
def m(obj)
obj.to_s + 1
end
m(0)
$ ruby -rdexc t.rb
0:lib/dexc.rb:82> tp.enable
TracePoint#enable: false
1:lib/dexc.rb:83> end
Dexc#start: false
2:lib/ruby/2.0.0/rubygems/core_ext/kernel_require.rb:45> return gem_original_require(path)
Kernel#gem_original_require: true
3:lib/ruby/2.0.0/rubygems/core_ext/kernel_require.rb:45> return gem_original_require(path)
Kernel#require: true
4:t.rb:0>
IO#set_encoding: #<File:t.rb (closed)>
5:t.rb:0>
IO#set_encoding: #<File:t.rb (closed)>
6:t.rb:1> def m(obj)
Module#method_added: nil
7:t.rb:2> obj.to_s + 1
Fixnum#to_s: "0"
8:t.rb:2> obj.to_s + 1
Exception#initialize: #<TypeError: no implicit conversion of Fixnum into String>
9:t.rb:2> obj.to_s + 1
Class#new: #<TypeError: no implicit conversion of Fixnum into String>
10:t.rb:2> obj.to_s + 1
Exception#exception: #<TypeError: no implicit conversion of Fixnum into String>
11:t.rb:2> obj.to_s + 1
Exception#backtrace: nil
TypeError: no implicit conversion of Fixnum into String
from t.rb:2:in `+'
from t.rb:2:in `m'
from t.rb:4:in `<main>'
From: t.rb @ line 2 Object#m:
1: def m(obj)
=> 2: obj.to_s + 1
3: end
[1] pry(main)> obj
=> 0
[2] pry(main)> hist[7] # or dexc_hist[7]
=> "0"
例外が起きた環境でREPL(例ではPry)が起動されています。 また、`hist'を使うことで例外発生直前の一連のメソッドの返り値を取得できることが分かります。
なお、トレースが取れるようになって便利になった反面パフォーマンスは低下しました。 参考までに、Rubyに含まれているテスト(test/ruby)を走らせた時の所要時間を比較すると以下のようになります。
| 条件 | 所要時間 |
|---|---|
| dexcなし | 2:25.83 |
| dexcあり(トレース無効(:raiseのみ)) | 2:28.90 |
| dexcあり(トレース有効) | 5:34.07 |
Ruby VM アドベントカレンダー が面白いので、 便乗してVMをテーマに書いてみます。
Ruby(CRuby)は1.9から評価器がVM化され、RubyプログラムはVM命令にコンパイルされてから 動作するようになりました。コンパイル結果は、rubyコマンドに--dump=insnsオプションを 与えることで確認することができます。
$ ruby --dump=insns -e '"abc"+"def"' == disasm: <RubyVM::InstructionSequence:<main>@-e>====================== 0000 trace 1 ( 1) 0002 putstring "abc" 0004 putstring "def" 0006 opt_plus <callinfo!mid:+, argc:1, ARGS_SKIP> 0008 leave
ここでは各VM命令の実行によってVMがどういう動きをするのか見る方法を紹介します。
やり方はいろいろあると思いますが、VM命令の先頭に 「VMの状態を出力してからTRAPを飛ばす」コードを 追加していくことにしましょう。
まず、rubyに以下のパッチを当ててビルドします。
gdb経由で実行してみます。(TRAPを飛ばすために環境変数ENABLE_TRAPを定義しておく必要があります)
$ ENABLE_TRAP= gdb --args ./ruby --disable-gems -e '"abc"+"def"' ... (gdb) r ... -- stack frame ------------ 0000 (0x7ffff6a09010): 00000008 0001 (0x7ffff6a09018): 555555a43d00 0002 (0x7ffff6a09020): 00000008 0003 (0x7ffff6a09028): 555555a39710 -- Control frame information ----------------------------------------------- c:0002 p:0000 s:0004 e:000858 EVAL -e:1 [FINISH] c:0001 p:0000 s:0002 e:000fa8 TOP [FINISH] ### Next: trace ################ Program received signal SIGTRAP, Trace/breakpoint trap. ... (gdb)
最初のVM命令であるtraceの実行直前のVMの状態(スタックフレームとコントロールフレーム)が出力されました。 ここでcontinue(c)と入力すると、
(gdb) c Continuing. -- stack frame ------------ 0000 (0x7ffff6a09010): 00000008 0001 (0x7ffff6a09018): 555555a43d00 0002 (0x7ffff6a09020): 00000008 0003 (0x7ffff6a09028): 555555a39710 -- Control frame information ----------------------------------------------- c:0002 p:0002 s:0004 e:000858 EVAL -e:1 [FINISH] c:0001 p:0000 s:0002 e:000fa8 TOP [FINISH] ### Next: putstring ################ Program received signal SIGTRAP, Trace/breakpoint trap. (gdb)
と、trace実行後の状態が出力されます。
続けてputstringを実行すると、
(gdb) c Continuing. -- stack frame ------------ 0000 (0x7ffff6a09010): 00000008 0001 (0x7ffff6a09018): 555555a43d00 0002 (0x7ffff6a09020): 00000008 0003 (0x7ffff6a09028): 555555a39710 0004 (0x7ffff6a09030): 555555a396e8 -- Control frame information ----------------------------------------------- c:0002 p:0004 s:0005 e:000858 EVAL -e:1 [FINISH] c:0001 p:0000 s:0002 e:000fa8 TOP [FINISH] ### Next: putstring ################ Program received signal SIGTRAP, Trace/breakpoint trap. (gdb)
スタックに値が追加されました。
rubyのソースツリーに含まれている.gdbinitには、 VALUEを与えるとそれをRubyオブジェクトとして出力するrpというコマンドが 定義されているのですが、それを使うと、
(gdb) rp 0x555555a396e8 T_STRING: "abc" bytesize:3 (embed) encoding:1 coderange:7bit $1 = (struct RString *) 0x555555a396e8
追加された値が"abc"であることが分かります。
興味がある人は、メソッド/ブロック呼び出しなどいろいろなコードを動かしてみると面白いかもしれません。