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

  1. トップ
  2. tech
  3. Ruby でスコープをはずれたときに自動で何かをする
▲ この日のエントリ