Monday, March 17, 2008

Move eval from Run-time to Parse-time

You need to use eval, but want to limit the number of times eval is necessary.

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

becomes

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

Motivation
premature optimization is the root of all evil -- Knuth, Donald
I'll never advocate for premature optimization, but this refactoring can be helpful when you determine that eval is a source of performance pain. The Kernel#eval method can be the right solution in some cases; but it is almost always more expensive (in terms of performance) than it's alternatives. In the cases where eval is necessary, it's often better to move an eval call from run-time to parse-time.

Mechanics
  • Expand the scope of the string being eval'd.
  • Test
Example

The following Person class uses eval to define the logic the readers rely upon for returning a default value if no value has previously been set.

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

The above example executes without issue, but it relies upon eval each time a reader is called. If multiple calls to eval are determined to be problematic the solution is to expand the eval to include defining the method itself.

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

3 comments:

  1. Anonymous10:03 AM

    At the risk of being a lot more verbose, in the example you've given you could use:

    ivar = "@#{attribute}"
    instance_variable_set(ivar, default_value) unless instance_variable_get(ivar)
    instance_variable_get(ivar)

    This would avoid using a string eval at all. My first inclination would be to use this, even without looking at performance numbers. I tend to avoid string eval whenever I can see an easy way out. What do you think? Should I not be so "scared" of eval in simple circumstances like this?

    ReplyDelete
  2. Anonymous10:09 AM

    In theory, I expect the database, network, etc to be the bottlenecks, not whether or not I use eval.

    In practice, I also avoid string eval, and eval in general if I can. I would also use instance_variable_set or instance_variable_get when possible.

    Pragmatically, we should probably both be less worried about the performance of eval and focus on writing the cleanest code possible.

    Also, there are times when you simply need to string eval. In my example, I want the default value to be anything that can be specified as a string. For example, you may not want EmployeeNumberGenerator to execute the next method until the employee_number method is called. In which case you'll need to pass in either a block or a string and eval when the reader is called.

    ReplyDelete
  3. Anonymous10:10 AM

    And, thanks for the comment. =)

    Cheers, Jay

    ReplyDelete

Note: Only a member of this blog may post a comment.