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]
担当: 齋藤 (eval があれば何でもできる)