アプリケーション稼働中ではなくて準備中の場合はevalを使ってもよい

Jay Fields' Thoughts: Move eval from Run-time to Parse-time

Rubyにおいて、実行時にevalするのはパフォーマンスが問題になることがある。文字列を解釈するコストが馬鹿にならないのだ。だからevalは極力避けるべき…なんだが例外がある。
ウェブアプリケーションなど起動時間はさほど問題ではなく、長時間稼動させるアプリケーションの場合、準備には多少時間をかけてよい。だから稼働中に実行するevalを準備中に移動するのは有効なリファクタリングだ。

Person.attr_with_defaultはデフォルト値つきのattr_readerだ。Person#emailsとPerson#employee_numberがデフォルト値つきで定義される。しかし、これらのメソッドを呼んだだけでevalされるのは効率面で問題だ。

class Person
  def self.attr_with_default(options)
    options.each_pair do |attribute, default_value|
      define_method attribute do
        eval "@#{attribute} ||= #{default_value}"
      end
    end
  end

  attr_with_default :emails => "[]", :employee_number => "EmployeeNumberGenerator.next"
end

リファクタリングして以下のようなコードにする。Module#define_methodをevalに置き換えたのだ。一瞬ぎょっとするかもしれないが、これは正しいリファクタリングだ。Person.attr_with_defaultは稼動時ではなくて準備中(定義した直後)に実行されるからだ。Person.attr_with_defaultの中でevalしてメソッドを定義している。

class Person
  def self.attr_with_default(options)
    options.each_pair do |attribute, default_value|
      eval "def #{attribute}
              @#{attribute} ||= #{default_value}
            end"
    end
  end

  attr_with_default :emails => "[]", :employee_number => "EmployeeNumberGenerator.next"
end

まぁ俺なら

module_eval %{
  def #{attribute}
    @#{attribute} ||= #{default_value}
  end"
}

と書くがな。Kernel#evalでもいいがModule#module_eval萌えだから。あと%{}がブロックらしく見えるので。

この場合ならObject#instance_variable_getとObject#instance_variable_setで書けるとコメントに書いてあった。まぁ当然のツッコミなんだが。でもメソッド名が長すぎるんだよね…

っつーか、準備中のevalで思い出したが、それってD言語のmixinじゃん!D言語のmixinはコンパイル時限定evalだから、コンパイラ言語なのにいろいろおもしろいことできるんだよね。