Codelogy

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 があれば何でもできる)

コメントを投稿

コメントの公開は承認制のため、投稿から掲載までに時間がかかることがあります。


About

2009年02月08日 23:10 に投稿されたエントリです。

他にも多くのエントリがあります。
メインページアーカイブページもご覧ください。