Eval() Isn't Quite Pure Evil
blog.grayproductions.net
blog.grayproductions.net
"The string is a stark data structure and everywhere it is passed there is much duplication of process. It is a perfect vehicle for hiding information."
(You're tired of the canonical link, so here's a much more fun one: http://www.emacswiki.org/emacs/perlisisms.el)
Strings hide information, and string representations of programming languages hide the most important things about programs (with the possible exception of programming languages designed to manipulate strings, like SNOBOL).
OTOH I like using ParseTree to convert Ruby programs into s-expressions and to Ruby2Ruby convert those s-expressions into strings before evaluating them. It's overkill at the moment, but somehow it seems to point towards something much, much better than strings:
%w[B C D].each do |name|
eval(
Ruby2Ruby.new.process(
s(:module,
name.to_sym,
s(:scope,
s(:defn,
:call,
s(:args),
s(:scope,
s(:block,
s(:call,
nil,
:puts,
s(:arglist, s(:str, name))
),
s(:zsuper))
)
)
)
)
)
)
end
It's ugly, but at least it tries to represent the structure of the Ruby code.When you program, do you think of it as an exercise in bending your mind to the whim of the computer, or the other way around? I, for one, would rather make the computer understand what I'm thinking.
If I'm thinking "hey, I want to evaluate this bit a bunch of times with different names", why can't I just say that? Isn't Ruby supposed to be the "English With Some Bits Of Punctuation" language? Assuming I've met any security concerns, and I'm not doing anything incredibly complicated (which, honestly, wouldn't pass code review with or without eval), what's wrong with writing the code the way I want?
Looking at the author's received "fix", I don't understand it. I'm not a big fancy Ruby expert, but I've been through the poignant guide, I've written some medium-sized stuff with it, I've used most of the syntax constructs, and I think I get the idioms. The original makes sense; it's clear to me. We do three string format operations, and eval each one, thus defining a module each time. Great.
The "fix" confuses me: "what's Module.new (I thought you just did 'module Name; stuff; end')?", "what's define_method (it looks like def, but why doesn't it just say def)?", "what's Object.const_set, and why don't I see 'name' until then?". With some reasonable inference, these aren't tough questions, but there remains one: "what the hell is the control flow diagram for this?". There are three anonymous functions defined in here, and I have no idea when the inner ones execute, or worse, are defined or compiled. I thought I understood Ruby's control flow, and I could make a guess at this, but there's no way I'd bet money on the result.
So what if eval turns code into strings? Code _is_ strings! Do you think files are made of parse trees? eval doesn't hide information, it adds another level of abstraction (and there's an old saying about solving problems in computer science somewhere in there). Syntax highlighters, test coverage reports, static analyzers, and cyclomatic complexity calculators (really?) should all be able to handle it. Why? Because the feature is there, and it makes my life easier sometimes, and in those times, I want to use it.
So, umm, there! That's what I think about eval.
The problem with eval-ing strings is that it doesn't scale. It's great for building three identical modules "B," "C," and "D," but the moment you want to do something interesting, it breaks down. The complexity of manipulating programs as strings grows far more rapidly than the complexity of things you want to do.
For example, String#to_proc adds some syntactic sugar to Ruby like implied parameters so you can write things like: some_array.map(&'+ 1'):
http://github.com/raganwald/homoiconic/blob/master/2008-11-2...
The implementation is hideous:
http://github.com/raganwald/homoiconic/blob/master/2008-11-2...
That's because it's trying to manipulate and eval strings that represent program syntax where it should be parsing the strings and manipulating data structures that represent program structure.
That's not the tool or technique's fault, of course. If people want to amend core classes with hundreds of kitchen-sink convenience methods or eval much more complex string manipulations... You might easily say that is the fault of the programmer for making a poor choice.
http://github.com/judofyr/camping/blob/fa8802511fd745dedcbea...
From there: "Recursion is the root of computation since it trades description for time."
I don't get it. Is recursion supposed to be slow?
[1] For those that had to look it up like I had to: http://en.wikipedia.org/wiki/Principle_of_Least_Power
A GOOD programmer can use such an extremely powerful programming construct and write good, readable code with it which just makes you go 'wow, this is ... just clean, and I get what it does in a second', which is nicer than any code one might produce without using these tools, even though everyone knows that this class of control constructs can create a horrible mess.
However, a BAD programmer can take this class of extremely powerful programming constructs, and create a horrible mess which requires more time to understand than a rewrite would take (more than once, probably).
And now, the third fact you need to add into the mix is: Bad programmers usually don't think they are bad programmers. The realization 'I am actually a pretty bad programmer' is one major step on the path to being a good programmer, as absurd as it sounds. Thus, a good programmer will usually not use this class of extremely power programming constructs, because he knows that he will probably produce a mess with them (and probably miss some really nice solutions), while a bad programmer will probably use them happily, thinking he produces clean and nice code (but he is not).
And this is the reason why gotos, evals and mind-bending meta-programming is not always bad, and not always evil, but we must not admit this, because otherwise, everyone will start using it :)
(yes, I am aware that this is some sort of black/white-imaging and the solution is somewhere in the middle for everyone, but it shows the point much nicer)
eval, on the other hand, is limited to whatever string the programmer types in. This may be shorter for a conference slide, but probably more painful in Real Software.
As to eval itself, it's obviously not 'evil'. But nontrivial use can make code unreadable:
doSomeStuff()
eval(someVar)
doOtherStuff()
this can be read as doSomeStuff()
doPrettyMuchAnythingConceivable()
doOtherStuff()
This neatly removes any ability to effectively reason about the code.Seems like decreasing the font size a few points would have clearer a better solution than clouding the could with loops and `eval()`/closures..
Depending on the context, perhaps showing the first class "full size", and abbreviating the other two would work.. or, show the code for one, then explain "we define another class named C, which prints C, and final one named D, which prints D" - it is a "talk", after all..
That's why I consider `eval` to be "evil" - there's almost always a better way..
eval(":" + symbol.to_s)
which converted a symbol to a ... symbol.One of the founders of Twitter mentioned seeing an Engine Yard guy copy a 500 meg file from one machine to another. That instance of the interpreter allocated 500 megs and wouldn't let it go till it is killed.
All this is the result of Ruby having to support Eval().
I love Ruby. I don't program in it anymore and I think that's tragic. I can see how Lisp is great just in the since that in a homoiconic language, eval is OK.
But Ruby is nice and so much more intuitive, productive and beautiful and welcoming than those homoiconic langs.
Perhaps this is why _why vanished... The Marcel Proust of the 21st century needs to write a tragic .... user's manual on the subject.
Ruby is neat and all, but is there really anything in ruby that's not dressed up smalltalk? Squeak doesn't randomly hang on to 500m, afaik.
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.