![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
![[livejournal.com profile]](https://www.dreamwidth.org/img/external/lj-userinfo.gif)
Ruby is a nifty little language that's had a small but devoted following for about 15 years. It's gotten a lot of attention recently with Ruby on Rails, a Web development framework that implements the model-view-controller design pattern for Ruby applications. Some things it does very nicely: its syntax is more highly reflective than any other language I can think of offhand since Smalltalk. At some point I should write something about the cooler parts of Ruby, but right now it's annoying me and so I'm afraid that today you get the bile.
There is always the risk, when writing an article like this one, of criticizing something simply because it is different from something that you happen to like more. I am sure this is no exception. I will try not to criticize Ruby for not being Lisp, or C, or heaven help me, Perl, but on the basis of its own merits.
First-class functions
Many methods in Ruby are accompanied by code "blocks" where other languages might use a closure or a reference to a subroutine, e.g.:
values.sort_by { |x| x.abs } palindromes = words.find_all { |w| w == w.reverse }
The confusing and maddening thing here is that you can't pass a function or a reference to one. Why not? Well, because it's not a block, and these methods will only accept a block. What's a block? A block is a chunk of executable code. You mean... like a function? Right. I mean, actually no, not really. What?
Trying to resolve this issue can reduce even the bravest warrior-hackers to a gibbering, cowering mess.
![[livejournal.com profile]](https://www.dreamwidth.org/img/external/lj-userinfo.gif)
Matz confirmed in a Ruby article in InformIT that this "block" construction is a syntactical oddity. It comes into play only when the interpreter sees an open brace (or the do keyword) supplied on the same line as an iterator method. Although it looks as though you are creating an anonymous procedure object and passing it as an argument to the iterator, the block isn't really any kind of first-class object and is not delivered to the iterator via standard argument-passing mechanisms. According to Bendersky, the interpreter quietly transforms the block into a closure and attaches it to a special slot on the iterator method, which the iterator can invoke with the yield statement.
This technique of invoking iterator methods with blocks is convenient but I find it unsatisfying and unsettling when examined more deeply. One of the commenters on Bendersky's essay asked, quite reasonably, why it's necessary to do this:
func = lambda { |x| x + 5 } func.call(2)
rather than the conceptually simpler
func = lambda { |x| x + 5 } func(2)
The answer is apparently that, because Ruby puts methods and local variables in separate namespaces, and permits methods to be called without parentheses, an expression like "func 2" would have an ambiguous parse tree. So you're not allowed to do it.
Alan Perlis once famously said that "syntactic sugar causes cancer of the semicolon." At long, long last, I think I finally understand what he meant.
Inconsistent variable scoping
Ruby variables are declared only in the arguments to Procs or methods. When a new free variable is instantiated, it is assigned to the innermost closure active at run time. Example:
def foo result = 0 (0..5).each do |n| result = n end end
Here exactly one result variable is instantiated across the entire method, and after the each loop terminates, its value is 5. However, in this example:
def foo (0..5).each do |n| result = n end end
the scope of the result variable is limited to the each block, because --- and this is important --- Ruby converts this block to a closure when the each method is invoked (see above). That is important because it explains why this other, semantically equivalent, code behaves differently:
def foo n = 0 while n < 5 do n = n + 1 result = n end end
See the difference? Because the block in the while loop isn't a closure, the result variable here belongs to the scope of the foo method. It is like the first example, except that in that case we had to force the variable's scope by deliberately instantiating it at the method level. That's not necessary here because the while block isn't a closure; only the each block is.
If you are anything like me, at this point you have already begun drinking heavily. Byzantine scoping rules like these are just a recipe for disaster.
Matz deserves a lot of credit for recognizing how bad this is. I have not had a chance to review what's happening with Ruby2, but the changes Matz describes in these slides look like a considerable improvement from the current state of affairs.
Anyway
Despite all this I can't really say that I dislike Ruby. I've been working in Ruby on Rails for only a few months, and have not really delved deeply into what the language has to offer, but I find a lot of its self-redefinition features really encouraging and intriguing. I can see that Matz has tried hard to strike a good balance between conceptual elegance and programming expressiveness, and that's a difficult line to walk. What makes me unhappy is where the language appears to violate the principle of least surprise; if issues like these can be remedied effectively, it will make Ruby substantially more appealing to hackers like me.
no subject
Date: 2008-03-10 04:31 pm (UTC)I always thought of that as one of those arcane things that C and C-like languages let you do to shoot yourself in the foot and several other body parts...and/or to ensure you have a job forever because that critical piece of code you wrote is completely unintelligible to anyone but you, and if they fire you the entire company will go up in flames of spaghetti.
I responded to this a bit below already, but Java OO code uses interfaces for (mostly) the same reasons that C would let you pass function pointers. There are situations where it's better to figure out what the right thing to do is during run time, not at compile time.