5
SEP2009
eval() Isn't Quite Pure Evil
I was explaining method lookup to the Lone Star Ruby Conference attendees and needed to show some trivial code like this:
module B
def call
puts "B"
super
end
end
module C
def call
puts "C"
super
end
end
module D
def call
puts "D"
super
end
end
Unfortunately, my space was limited due to the code being on slides and me needing to show more than just what you see above. I cracked under the pressure and committed two major programming sins. First, I collapsed the code with eval()
:
%w[B C D].each do |name|
eval <<-END_RUBY
module #{name}
def call
puts "#{name}"
super
end
end
END_RUBY
end
Then I really blew it when I jokingly apologized to the audience for using eval()
.
I got the first email with corrected code before I even finished the speech. OK, the email was from a friend and he wasn't mean, but he still instantly needed to set me straight. I had clearly turned from the light.
Only, his corrected code didn't quite work. It got close, but it had bugs. Still the approach was sound and it could be made to work. Let me fix the bugs and show you what was recommended:
%w[B C D].each do |name|
mod = Module.new do
define_method :call do
puts name
super()
end
end
Object.const_set(name, mod)
end
Is that better? It is a line shorter and it avoids the use of eval()
, which we all know to be pure evil. I found it a bit harder to follow the closure logic here though and remember that the point was to show clear code on a slide. I also wouldn't want to have to explain to anyone why the parentheses are required on the super()
in that example, if only because it has nothing to do with the real point of the code.
That leads us to the natural question: why is eval()
pure evil?
I hear two complaints most often when this talk comes up. The first is that eval()
is dangerous. That usually means that we could take in some malicious code from a user and pass it into eval()
where it would destroy our hard drive, or worse. Only, I'm not doing that. I made the variables here and user input is not involved. I think we're safe this time.
The other reason people tend to complain is that it's too much tool for the job. What does that mean? Is it slower because it includes extra busy work for the computer? Nope:
$ cat time_eval.rb
#!/usr/bin/env ruby -wKU
require "benchmark"
TESTS = 10_000
Benchmark.bmbm do |results|
results.report("eval():") { TESTS.times {
%w[B C D].each do |name|
eval <<-END_RUBY
module #{name}
def call
puts "#{name}"
super
end
end
END_RUBY
end
} }
results.report("closures:") { TESTS.times {
%w[B C D].each do |name|
mod = Module.new do
define_method :call do
puts name
super()
end
end
Object.const_set(name, mod)
end
} }
end
$ ruby time_eval.rb 2> /dev/null
Rehearsal ---------------------------------------------
eval(): 0.940000 0.080000 1.020000 ( 1.027164)
closures: 1.070000 0.150000 1.220000 ( 1.216268)
------------------------------------ total: 2.240000sec
user system total real
eval(): 0.950000 0.080000 1.030000 ( 1.034849)
closures: 1.070000 0.150000 1.220000 ( 1.219367)
Every time I tried that test eval()
came out the same or faster. It always seems to be faster on the calling side:
$ cat time_eval_calls.rb
#!/usr/bin/env ruby -wKU
require "benchmark"
class A
def call
warn "A"
end
end
%w[B C D].each do |name|
eval <<-END_RUBY
module #{name}
def call
warn "#{name}"
super
end
end
END_RUBY
end
class E < A
include B
include C
include D
def call
warn "E"
super
end
end
class F
def call
warn "F"
end
end
%w[G H I].each do |name|
mod = Module.new do
define_method :call do
warn name
super()
end
end
Object.const_set(name, mod)
end
class J < F
include G
include H
include I
def call
warn "J"
super
end
end
TESTS = 10_000
Benchmark.bmbm do |results|
results.report("eval():") { TESTS.times { E.new.call } }
results.report("closures:") { TESTS.times { J.new.call } }
end
$ ruby time_eval_calls.rb 2> /dev/null
Rehearsal ---------------------------------------------
eval(): 0.210000 0.100000 0.310000 ( 0.315426)
closures: 0.270000 0.120000 0.390000 ( 0.389828)
------------------------------------ total: 0.700000sec
user system total real
eval(): 0.210000 0.100000 0.310000 ( 0.314385)
closures: 0.270000 0.120000 0.390000 ( 0.390397)
That makes sense if you think about it. The closure version will need to hunt through the scopes and resolve those variables with each call. The eval()
call builds a normal Ruby method though, so once it executes it's just a normal method call.
I guess I'm all out of objections.
I agree it's sometimes worth avoiding eval()
. However, I think we need to admit that it's sometimes OK to use eval()
. The trick is to ditch the fear and rationally evaluate which of the two you are facing.
Comments (24)
-
Darrick Wiebe September 5th, 2009 Reply Link
I absolutely agree that
eval()
should have a place in any Ruby metaprogrammer's toolbox. In most caseseval()
is the simplest solution that works, and should be chosen on that basis alone.I think that
eval
ing aString
should be considered the preferred technique for defining methods on the fly or performinginstance_eval
s orclass_eval
s unless certain conditions are met which tilt the balance in favor of using a block. If I need to pull variables out of the scope that is defining my meta code, and if they are not simply basicString
s,Symbol
s orNumeric
s, then it makes sense to spring for the conceptual overhead of understanding the block scoping of theeval
method you need. Otherwise—and this is most cases—it's probably best to interpolate what you need and into theString
andeval
it.In most cases, the small difference in performance between the two options is not worth considering and in order to avoid the trap of premature optimization it should not be.
None of what I'm saying here takes into account the possibility of
eval
ing user input, and if anyone is considering doing any kind of metaprogramming based on user input, their most important decision is probably not which kind of gun to shoot themselves in the foot with anyway. -
eval()
is evil because it turns your code into aString
.String
s are for data, and putting your code into data destroys the ability of all sorts of useful tools, like code coverage, cyclomatic complexity calculators, and all sorts of other static analyzers—I'm not even going to mention editor syntax highlighting.Now, the proper solution is a real homoiconic language, but Ruby isn't going to go there any time soon. So, while
eval
is evil, it can't be helped in Ruby. But, please, use it as sparingly as often! -
I know you didn't want to explain the parenthesis on
super()
on the slide, but would you be so kind as to explain it here?-
I'm happy to explain the required parentheses, yes.
super
is one of those magical keywords in Ruby. It often looks like a normal method call, but it has a little magic involved. When you callsuper
without any parentheses, it invokes a method of the same name higher in the call chain passing the same arguments that the method which invokedsuper
received. That's the magic. You can choose to specify the arguments yourself, with or without parentheses, overriding the magical behavior. The default for a bare call though is the magic.eval()
can define normal Ruby methods, which is how I used it in my example. Because of that, you get all the normal magic andsuper
works the same. In my example, thesuper
's forward all arguments up the call chain. There just don't happen to be any arguments to forward.As I hinted at in the article though,
define_method()
is kind of a second class citizen. It builds a method for you, but you don't quite get all of the normal method magic.super
is one of the cases where the magic doesn't work. To protect you from bugs, Ruby tosses an error when it sees a baresuper
inside of adefine_method()
call. The reason is that it assumes you wanted the magic and it can't give it in this case, so the error serves as a warning that your expectations may be off.In my example though, I didn't need the magic. I have nothing to pass up. Even though I can usually do that with a bare method call, this is one of the exceptions. A bare
super
call would summon the no-magic-here warning error. Thus, I have to explicitly pass zero arguments with the empty parentheses.See why I didn't want to have to explain that to the audience? Yuck!
-
-
Hey James, great article. It's great that we're challenging notions of proper style and practice all the time (yet another reason why Why the Lucky Stiff was my hero, RIP).
Anyways, wanted to chime in that I think there's also a difference in execution position as well in that evaling dynamic methods and modules at load time is absolutely fine but that runtime might be less ideal. I like to have most of my dynamic evaluation happen at once in the beginning before the "live" state of the application (for long-running apps mostly).
Thanks for sharing!
-
eval
is evil, they even sound the same. -
I'm curious. If Ruby didn't have
eval()
at all, what (rational) things couldn't you do with eval-less Ruby that could be done with Ruby as it exists today? I mean, there have been a few times where I thought I had a legitimate use case foreval()
, but thus far, I've almost always ended up being proven wrong once I get far enough along in the development process. Template languages are the only significant legitimate use I can think of, that couldn't easily be handled by some other facility of the language.-
That's an interesting thought experiment, and you're right that there are very few things that you can do with
eval
that you couldn't do without.But if you're suggesting that as an argument against having
eval
in the language ... well, if we had the rule that we only wanted one way to do something in the language, half of Ruby would have to go, and we'd just have Python. ;-) -
If Ruby didn't have
eval()
at all, what (rational) things couldn't you do with eval-less Ruby that could be done with Ruby as it exists today?The answer is metaprogramming without closures (the latter being potentially costly both in terms of namespace resolution and efficient garbage collection, as well as semantically undesirable and occasionally productive of hard-to-track-down bugs).
-
-
Whenever I come across
eval
in Rails code it is usually a method call where either the class, method, or arguments are some variable. I don't likeeval
in that use case and prefer usingsend
. Personally, I findString
programming harder to read, which makes it harder to maintain.Also, while it isn't a wise to write code based on limitations of your editor, there may be an argument to be made on the cost of losing out on syntax highlighting on a chunk of code.
-
I'm usually not one to go all Eval Knieval on my code, but there was one case where
eval
seemed to be my only viable option. I was doing some metaprogramming and needed to choose a class on the fly.Object.const_get
does not work with namespaced classes, so the quickest way was to useeval("Namespace::ClassName")
.-
Wyatt: It may have been possible to use code like the following in your case:
"Namespace::ClassName".split("::").inject(Object) { |a, c| a.const_get(c) }
-
-
I think you are correct. I think that often times as technologists we want shortcuts since there are so many things to remember day to day. However as technical people we ought to find out why we repeat a mantra, not simply repeat it.
Thanks for the reminder.
-
It reminds me of a story a friend once told. A girl is watching her mother cook leg of (something), and noticed that before she put it in the oven, the mother always cut off both the ends. Asked why, the mother replies "That's what my mother always did". So she goes to the grandmother, and she gets the same response. So she goes to her great grandmother, and she explains "Well, it was during the war and money was tight. We didn't have an oven, and I could never fit it into the saucepan, so I always had to chop the ends off"
The moral of the story: Always ask why. Otherwise you're wasting useful parts of the (meat|language) based on effectively superstition. In this case, if
eval
does what you need and you are aware of the possible dangers, never feel pressured into doing the same thing in a more convoluted way based on someone else's superstitions.-
I can't resist adding that in Primo Levi's wonderful book "The Periodic Table" there is a section called "Chrome" which is essentially a more literary (and true—it actually happened to Primo Levi when he was an industrial chemist working for a paint factory) version of the story in Matthew Bennett's post.
-
-
If you feel your audience wouldn't understand that
super()
call then you probably shouldn't useeval
in front of them. They may not understand when to ignore theeval
mantra.I'd argue this is the beauty of a mantra. By the time you question it you know enough about the pros/cons to safely make your own judgment calls.
-
I recently asked a question on StackOverflow about why using
goto
would be preferable to runtime code evaluation witheval
, in the context of a programming assignment in a graduate class. In amongst everyone trying to tell me to use recursion, there were some good points made about whyeval
can be bad:- Syntax errors in Ruby strings to be
eval
ed will not be found by your IDE. - Code in strings to be
eval
ed can be harder to read than more complicated code that is not in strings.
- Syntax errors in Ruby strings to be
-
There are two main reasons I dislike
eval
in that situation, and prefer the closure solution.First, eval breaks syntax highlighting in my editor. For this reason alone, I find the closure logic easier to follow.
And second, it's not only dangerous for security reasons, it's dangerous in any case the developer (or a user) might pass something into those variables other than what you intended, including a typo. In an example like that, suppose the
name
variable somehow had a double quote or a#{
in it… Contrived, sure, but when things like that do happen,eval
makes them that much more difficult to debug, whereas it might have not even been an issue withdefine_method
.I agree it's not always evil—for example, I'd already have guessed that appropriate use of
eval
can improve performance, just as inappropriate use can destroy performance. But premature optimization is the root of all evil—once I've narrowed it down to adefine_method
being a bottleneck, I'll considereval
, but not before.And there are at least two things which could not be done in an
eval
-less ruby, last I checked:Access to local variables through bindings. (I'm not sure if 1.9 fixed this...) This is one example of what I think is an appropriate use of
eval
, even disregarding performance—find some feature the language doesn't provide, implement it, tuck theeval
deep inside some library where no one has to see it again, and be done with it.And development tools—irb, debuggers, IDEs, etc. This goes back to Lisp's read-eval-print-loop, and whenever I'm forced to use a language without an interactive prompt of some kind, I always feel crippled.
They do make templates more convenient, but technically aren't required—you could write a "compiler" which translates a template into Ruby, but I'd argue this is a legitimate use for
eval
, at least if you want to allow Ruby code in the template.-
eval
isn't always evil though, I think it's mis-used and misunderstood.Binding#eval
is extremely useful for prying inside the scope of aProc
to gain access to the value of a variable, and obviously, for a REPL it is essential too ;-)eval
is beat on a lot but only because people mistakenly use it for something you shouldn't use it for, it does have some valid use cases, though.
-
-
Your code James Edward Gray II
is evil
ruby_lang/programs/see-no-eval.rb:34: warning: already initialized constant B (eval):1: warning: previous definition of B was here ruby_lang/programs/see-no-eval.rb:34: warning: already initialized constant C (eval):1: warning: previous definition of C was here ruby_lang/programs/see-no-eval.rb:34: warning: already initialized constant D (eval):1: warning: previous definition of D was here
But oh no don't show us things like that. Blog on brother.
But if you could just stick to ERB you'll be fine.require 'erb' # Create template. template = %q( From: James Edward Gray II <james@grayproductions.net> To: <%= to %> Subject: Addressing Needs <%= to[/\w+/] %>: Just wanted to send a quick note assuring that your needs are being addressed. I want you to know that my team will keep working on the issues, especially: <%# ignore numerous minor requests -- focus on priorities %> % priorities.each do |priority| * <%= priority %> % end Thanks for your patience. James Edward Gray II ).gsub(/^ /, '') message = ERB.new(template, 0, '%<>') # Set up template data. to = 'Community Spokesman <spokesman@ruby_community.org>' priorities = ['Run Ruby Quiz', 'Document Modules', 'Answer Questions on Ruby Talk'] # Produce result. email = message.result puts email
I guess that you were just trying to document your M0dules?
Thanks! But what was your talk all about?
Practicing Ruby? There's good articles there about method lookup and Modules. I think he even mentions you James.
P.S. HEREDOCS are evil-
But oh no don't show us things like that.
Sorry, but I don't really understand the complaint or how you got these warnings.
-
-
This is all you would need in the mean time.
# module B def call puts 'B' super end end # module C def call puts 'C' super end end # module D def call puts 'D' super end end
According to Rubocop.
-
Now back to the topic. Doesn't IRB use eval() extensively?
Have you considered integrating something like that into you testing?
Perhaps even Pry would work too.
I just don't know enough to go about doing so myself.
Perhaps you do James.
Any way it would be an interesting article and I know that Rack has a middleware
that I've seen some rails apps use.