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 をラップして使うのがよさそう、あるいはもっと別の方法を考えたほうがよさそうです。継続を使う方法が一番カッコいいと思いますが、予期せぬことがありそうなので、できれば組込みクラスでやったほうがいいですね。なにかもっといい方法があれば教えてください。