Guard オブジェクト
Perl だと Guard オブジェクトとかいうハックがあって、スコープを出るタイミングで必ず呼ばれるファイナライザを使って、あるスコープでだけ有効な処理を書けたりします。
例えば、DB のトランザクションや、あるいは以下のように依存するプロセスをあるスコープでだけ起動して終了するような用途で使われています。
{ my $guard = Proc::Guard->new(command => [ "memcached", "-p", "12321" ]); # do something ... }; # memcached has been killed
適当なメソッドにブロック(サブルーチン)を渡せばええやん、という気もしますし、実際 Ruby の transaction の場合そういう感じになります (Perl でももちろん同じようなサブルーチンを書くことはできます)。
ActiveRecord::Base.transaction do .... end
しかし Perl の Guard オブジェクトと同じようなことをしたい! と思うと、Ruby の場合 finalizer の呼ばれるタイミングが GC が動いたタイミングなので、finalizer は使えません。
なので、多少妥協する必要があります。
複数のブロックのネストをフラットにする
上記のように依存プロセスを起動して勝手に終了してほしい、みたいな場合、ドンドコ他のプロセスも増やしていきたくなります。Ruby で書くなら以下のような感じでしょうか (実際このケースだと process メソッドなんて作らず単に begin / ensure / end を纏めて書いちゃうと思いますけど)
def process(cms, &block) fork { exec *cmd } yield ensure Process.kill(:INT, pid) end process( [ "memcached", "-p", "12321" ]) do process(["foo"]) do process(["bar"]) do .... end end end
ドンドコ作るとドンドコネストしまくっていきます。しかし、1個増えるたびにいちいち中身全部インデントしなおしていったらダルくて嫌な感じです。
Enumerator を使う
ブロックを持つ process をフラットにしようとしてみます、とりあえず Enumerator を使うのがお手軽そうです。
begin e1 = to_enum(:process, [ "memcached", "-p", "12321" ]) e2 = to_enum(:process, ["foo"]) e3 = to_enum(:process, ["bar"]) e1.next; e2.next; e3.next .... ensure [e1, e2, e3].each do |e| begin e.next rescue StopIteration end end end
フラットにはなりましたが、何をやっているのか大変謎になりました。イテレーション (繰替えし) してないのに next とか StopIteration とか出てきて意味不明ですし、処理が上下にちらばっているのが嫌な感じです。これならネストが増えるほうがマシな感じがします。
Enumerator をラップする
そこで Enumerator を適当にラップしてみます。以下のようなメソッドを定義します。
def guard_scope(&block) context = Object.new enums = [] context.define_singleton_method(:guard) do |obj, name, *args| enum = obj.to_enum(name, *args) enums.unshift(enum) enum.next end ret = context.instance_eval(&block) enums.each do |enum| enum.next rescue StopIteration end ret end
そして使う場合
guard_scope do g1 = guard(Kernel, :process, [ "memcached", "-p", "12321" ]) g2 = guard(Kernel, :process, ["foo"]) g3 = guard(Kernel, :process, ["bar"]) p :in_block end
これでかなりマシになった気がします。
(ただ、これだと instance_eval を使っている関係でインスタンス変数のスコープが変わるのでよくありません)
継続を使う
Enumerator をラップすれば十分そうですが、どうも自分はメソッドの名前を Symbol にして渡して呼ぶのが好みではないので、以下のように書けたらいいなと思いました。
guard_scope do g1 = guard { process([ "memcached", "-p", "12321" ], &block) } g2 = guard { process([ "foo" ], &block) } g3 = guard { process([ "bar" ], &block) } p :in_block end
こうなると、Enumerator を使いにくいのでちょっと面倒です。継続を使って実装することにしました。spec コード付き
require 'continuation' class Guard attr_reader :args attr_reader :called attr_reader :result def initialize(&before_finish) @args = nil @guard_cc = nil @block_cc = nil @finish_cc = nil @before_finish = before_finish end def finish(result=nil) @before_finish.call(self) cc = callcc {|cc| cc } if cc.is_a? Continuation @finish_cc = cc @result = result @called = true @block_cc.call(@result) # return finish block else cc end end private def return_guard @guard_cc.call(self) end def return_finish(ret) @finish_cc.call(ret) end end def guard_scope(&block) context = self guards = [] current_guard = nil orig_guard = begin context.singleton_class.method(:guard) rescue NameError; nil end orig_block = begin context.singleton_class.method(:block) rescue NameError; nil end context.define_singleton_method(:guard) do |&b| cc = callcc {|cc| cc } if cc.is_a? Continuation current_guard = Guard.new do |g| current_guard = g end current_guard.instance_variable_set(:@guard_cc, cc) # Expect &block syntax and # A method in block call block's lambda to get args # and return guard ret = b.call if current_guard.instance_variable_get(:@block_cc) # After finish of a guard # current_guard is set by finish (block passed to Guard.new) g = current_guard current_guard = nil g.send(:return_finish, ret) else raise "guard {} without calling &block" end else current_guard = nil cc end end context.define_singleton_method(:block) do count = 0 lambda {|*args| raise "block is called more than once" if count > 0 count += 1 cc = callcc {|c| c } if cc.is_a? Continuation current_guard.instance_variable_set(:@args, args) current_guard.instance_variable_set(:@block_cc, cc) guards.unshift(current_guard) current_guard.send(:return_guard) else cc end } end block_value = context.instance_eval(&block) if orig_guard context.define_singleton_method(:guard, &orig_guard) else context.singleton_class.send(:remove_method, :guard) end if orig_block context.define_singleton_method(:block, &orig_block) else context.singleton_class.send(:remove_method, :block) end guards.each do |g| g.finish(nil) unless g.called end block_value end
なんかだいぶ長くなりましたが、とりあえずこれで動くようになりました。
やってることはただの辻褄あわせで、一度ブロック付きで普通に呼んで、すぐ継続を作って guard {} を返してから、最後にまた保存した継続を実行しているだけです。
(この例は instance_eval を使わず、呼び出し元 self に一時的にメソッドを生やしているのでインスタンス変数のスコープが変わったりしないようになっています)
set_trace_func を使ったメソッド単位の Guard
set_trace_func を使えばメソッド単位の突入・脱出はとれるので、それで Scope::Guard 的に、先にブロックを登録しておくとスコープをはずれるときに自動実行するのを実装してみました。インターフェイス的には以下のようになります。
instance_eval do # 任意の適当なメソッド guard { p "called as leaving this method 1" } guard { p "called as leaving this method 2" } end
module Guard @@guards = [] def guard(&block) set_trace_func(lambda {|event, file, line, id, binding, klass| # p [event, file, line, id, binding, klass] case event when "call", "c-call" for g in @@guards g[:count] += 1 end when "return", "c-return" out_of_scope = @@guards.each {|g| g[:count] -= 1 }.select {|g| g[:count] < 0 } @@guards -= out_of_scope for g in out_of_scope g[:block].call end set_trace_func(nil) if @@guards.empty? && id != :set_trace_func end }) if @@guards.empty? @@guards << { block: block, count: 1 } end end include Guard require "rspec" RSpec::Core::Runner.autorun describe Guard do before do @events = [] end it "should execute guard block as it is out of scope" do instance_eval do # new scope @events << :enter guard { @events << :end1 } guard { @events << :end2 } @events << :leave end expect(@events).to eq([ :enter, :leave, :end1, :end2 ]) end it "should works correctly with nested scope" do instance_eval do # new scope @events << :enter1 guard { @events << :end1_1 } guard { @events << :end1_2 } instance_eval do @events << :enter2 guard { @events << :end2_1 } guard { @events << :end2_2 } @events << :leave2 end @events << :leave1 end expect(@events).to eq([ :enter1, :enter2, :leave2, :end2_1, :end2_2, :leave1, :end1_1, :end1_2, ]) end end
割とシンプルに書けるんですが、set_trace_func が1個しか登録できないので、他のところで登録されると壊れるのと、スコープ単位ではなくメソッド単位にしかならないのが微妙なところです。良いところは、任意のメソッド内でいきなり guard を呼べるところで、これのためにブロックをつくる必要がない。
Scope::Guard 的なのの一番シンプルな実装
単に Scope::Guard 的に先にブロックを出たときの処理を登録するだけなら、もっと簡単に以下のように書けます。
イテレータ的なメソッドでなければ、ブロック引数をとる形式以外に、File.open のようにブロックを与えないときは自分で close するメソッドも提供されているので、大抵の場合はこれでもよさそうな気がします。
def guard_scope(&block) context = Object.new guards = [] context.define_singleton_method(:guard) do |&b| guards << b end context.instance_eval(&block) ensure guards.each {|g| g.call } end guard_scope do p :enter guard { p :end1_1 } guard { p :end1_2 } guard_scope do p :enter guard { p :end2_1 } guard { p :end2_2 } p :leave end p :leave end
まとめ・感想
Guard オブジェクトのやりかたは無闇にインデントが増えず、なおかつ関連する処理を近くに配置して確実なコードを書けます。そんなこんなで自分は好きなのですが、簡単にできなそうなので、なんとかしていい感じにやる方法を考えてみました。
基本は Enumerator をラップして使うのがよさそう、あるいはもっと別の方法を考えたほうがよさそうです。継続を使う方法が一番カッコいいと思いますが、予期せぬことがありそうなので、できれば組込みクラスでやったほうがいいですね。なにかもっといい方法があれば教えてください。