Ruby でローカルスコープを作る
必要に迫られて、ローカルスコープを作り出す関数を作ってみました。
背景
私が CGI を作る時には、基本的に Ruby で実装し、html の出力の際には eRuby を使っています。 小さいページを作るときはあまり問題にならないのですが、結構な規模のページを作ろうと思うと、テンプレートが長大になってしまいます。 さらに、いくつかのページでは重複する部分 (ヘッダやフッタ) が存在し、すべてのテンプレートにそれを記述しておくのは冗長で、変更が面倒になります。
そこで、テンプレートを複数に分割し、別のテンプレートを埋め込めるように、以下のような関数を定義して使用しています。
def include_template(filename, binding) # 再帰的に include_template を呼ばれると, ERB でバッファが競合するので, それを # 避けるためにサフィックスに一意な番号を付ける. @erb_buffer_count = 0 unless @erb_buffer_count buffer_name = '_erbout_' + @erb_buffer_count.to_s @erb_buffer_count += 1 erb = ERB::new(IO.read(filename), nil, '%', buffer_name) erb.filename = filename res = erb.result(binding) @erb_buffer_count -= 1 return res end # def include_template |
例えば、以下のように使います。
<html> <head> <title>test</title> </head> <body> <%= include_temmplate('header.rhtml', binding) %> <div id="main"> Hello, world! </div> <%= include_temmplate('footer.rhtml', binding) %> </body> </html> |
スコープが分けられない
これで、テンプレートが分割でき、開発効率も改善されたのですが、テンプレートを分割していった過程で新たな問題が出てきました。 それは、それぞれのテンプレート間で自由に変数を使っていたために、予期せぬ内に変数が変更されていることがある、という問題でした。
普通、このような問題はスコープを分けることで減らすことができるのですが、eRuby のテンプレートでは、うまく分けられません。 というのも、現在の Ruby では、あるスコープ内で完全にローカルな変数を確実に作る方法は関数定義、クラス定義、モジュール定義のいずれかしかないにも関わらず、eRuby では、間にテンプレートを含む関数、クラス、モジュールを定義することができないからです。 例えば、以下のようなテンプレートはエラーが起きて動きません。
% def hello(name) Hello, <%= name %>! % end <%= hello('world') %> |
なぜなら、上のテンプレートは以下のように展開され、hello 関数からは出力バッファである _erbout が視えないからです。
_erbout = ''; def hello(name) _erbout.concat "Hello, "; _erbout.concat(( name ).to_s); _erbout.concat "!\n"; end _erbout.concat "\n"; _erbout.concat(( hello('world') ).to_s); _erbout.concat "\n"; _erbout |
ここで、Proc や begin 〜 end でブロックを作るという方法を考えられるかもしれませんが、そもそもこれらは常に完全なローカル変数を作り出すことはできません。 ローカル変数を作り出せるのは、そのブロックの外のスコープに同名のローカル変数が定義されていない場合だけなのです。 それ以外では、外側のスコープの変数にアクセスしているとみなされます。
a = 1 b = 2 p [a, b] # => [1, 2] begin b = 3 p [a, b] # => [1, 3] end p [a, b] # => [1, 3] | a = 1 b = 2 p [a, b] # => [1, 2] Proc.new{|b| b = 3 p [a, b] # => [1, 3] }.call(b) p [a, b] # => [1, 3] |
ちなみに、このような問題は結構前から指摘され、そのうち対策案が実装されるようです。 参考: Ruby に let/local/my がない(らしい)ことについて [Ruby] - Rainy Day Codings
解決
しかし、いつ実装されるのかよく分からないので、自分でローカルスコープを定義する関数を作ってみました。
def local_vars(vars, binding) vars.each{|sym| eval(<<EOS, binding) _local_var_stack_ = [] unless _local_var_stack_ _local_var_stack_.push(defined?(#{sym}) ? #{sym} : nil) EOS } yield() vars.reverse.each{|sym| eval("#{sym} = _local_var_stack_.pop", binding) } end # def local_vars |
個人的に eval は好きではないのですが、ローカルスコープを作るためなら仕方ないということで、諦めました。 それはさておき、これにて以下のようにローカルスコープを作ることができるようになりました。
a = 1 b = 2 c = 3 p [a, b, c] # => [1, 2, 3] local_vars([:b], binding){ b = 4 p [a, b, c] # => [1, 4, 3] local_vars([:c], binding){ c = 5 p [a, b, c] # => [1, 4, 5] } p [a, b, c] # => [1, 4, 3] } p [a, b, c] # => [1, 2, 3] |