Ruby On Nails Scratching a Chalkboard 52

Posted by Christopher Smith Tue, 12 Sep 2006 17:59:00 GMT

So, as I explore how Ruby works, I’m discovering some bits of ugliness. It’s syntax is increasingly reminding me more of Perl than Smalltalk. A case in point: blocks.

I’d heard so much about Ruby’s Smalltalkishness that I was a bit taken aback when I saw control statements in the language grammar. In Smalltalk, control flow is managed using methods and blocks, and I knew Ruby had blocks (this is one of the things that you hear so much about in Beyond Java), so why did they need these control statements? In Smalltalk, control flow looks like this:

1 + 1 = 2
  ifTrue: ['it is true']
  ifFalse: ['it is false']

Now, I can’t claim that this provides any real productivity boost over Ruby’s approach:

if (1 + 1 == 2) then
  'it is true'
else
  'it is false'
end

But I was kind of surprised, given Ruby’s ties to Smalltalk, that someone hadn’t hacked it in. So, I went about hacking it in myself. That’s when I found out why.

It turns out that blocks in Ruby have a very high level of syntactic sugariness. Not only do they have their own special literal form (which is a key advantage over say Java’s Inner Classes, or C++ functors without boost::lambda), but they also have their own special status which really makes them non-objects. (I found it amusing to discover that the most non-object entity in Ruby is a block).

Here’s the magic: blocks aren’t passed as normal parameters to functions. They are passed through an implicit variable (showcasing Ruby’s Perlishness here). So, if, for example, I wanted to add something like Smalltalk’s ifTrue: to Ruby, I’d do the following:

class TrueClass
  def ifTrue
      yield
  end
end

class FalseClass
  def ifTrue
  end
end

(1 + 1 == 2).ifTrue { puts 'Math works' }
(1 + 3 == 2).ifTrue { puts 'Math is broken' }

Notice that ifTrue doesn’t appear to take any parameters, and neither does the “yield” method. In reality, the block is an implicit parameter. One Ruby tutorial claimed this is a good thing, because it means that all Ruby methods can take a block as a parameter…. even if they don’t use it. Me, I’m a big fan of explicitness, but I can see that in a scripting world, sometimes these kind of shortcuts are nice to have. What’s bad about this is that not only does it mean that all Ruby methods can take a block as a parameter, it also means all Ruby methods can only take exactly one block as a parameter, and it has to be the last one.

Now, it turns out that Ruby has a wrapper around blocks called Proc, which lets you treat a block like a real object, Of course, it has all the syntactic beauty of Java’s Inner Classes. Here’s how you can do ifTrueifFalse in Ruby:

class TrueClass
  def ifTrueIfFalse(trueProc, falseProc)
    trueProc.call
  end
end

class FalseClass
  def ifTrueIfFalse(trueProc, falseProc)
    falseProc.call
  end
end

(1 + 1 == 2).ifTrueIfFalse(Proc.new { puts 'Math works' },Proc.new {puts 'Math is broken'})

But wait! There’s more! Since Proc’s are proper objects, you can query them for meta-information, which is really handy for various dynamic programming tricks. Only… Ruby’s interface is kind of weird. Proc’s have this method “arity” which tells you how many arguments the block takes… sort of. For reasons passing understanding, if a block takes zero arguments, the function returns “-1” intead of “0”, and if it takes 1 argument, it returns “-2” instead of “1”. So, now we’ve established that it can never return 0 or 1, and that you can’t always use the return value as an collection size for your argument list. Here’s where it gets really crazy though: if your function takes a variable argument list with it’s last parameter, arity returns “0 - # of args”. So, quick question for you: if arity returns back -2, does that mean it’s argumetn list is one argument long, or that it takes one argument followed by a variable list of arguments? I’m not sure how Bruce Tate can claim that Ruby doesn’t have some weird anachronisms that get in the way of doing metaprogramming with a straight face.

In fairness, the case where you want to pass a single block as your last argument seems like the common case, and Ruby is a scripting language after all. I’m mostly annoyed because I’ve heard so many people talk about Ruby’s elegance, comparing it favourably with Smalltalk (which admittedly is not entirely without warts). Upon inspection it seems to have warts just like other languages (well, some languages have a few more warts than others). Still, there is hope. Ruby does seem to have some genuinely nice features, and it is open source, so there is always the possibility that some of these idiosyncracies will get cleaned up in the future.

UPDATE: So, someone with some real Ruby experience has clarified for me that nobody actually does “Proc.new” in Ruby. Instead they use Lambda. So, invoking my ifTrueIfFalse method would normally be done like so:

(1 + 1 == 2).ifTrueIfFalse(lambda { 'Math Works' },lambda {'Math Doesn't Work' })

Which I have to admit does seem a lot prettier for some reason.

ANOTHER UPDATE: I’ve gotten some great comments to this article, and I thought I should incorporate their content. First, people have suggested that you can break up ifTrueIfFalse in to two calls that are chained together, and then get back some of the elegance. I thought about this when I first looked in to it, but you lose the ability to pick up a return object cleanly.

Antti Tarvainen provided some excellent points. In particular he clarified the difference between a Proc that takes no arguments (arity returns 0) and a Proc that doesn’t define any arguments returns -1. Furthermore, arity has been updated for Ruby 1.9 to what seems like a more sensible behavior. I noticed that even in 1.8

puts lambda {|a|}.arity
returns 1, which suggests the Ruby documentation is a wee bit out of date.

I still think it’d be far more sensible to not overload the arity method and instead have numArgs? which gets you the number of required arguments”, hasOptional? which gets you back a boolean as to whether there are optional arguments, and argsDefined? which gets you back a boolean as to whether the Proc has defined arguments at all. Overloading the meaning of the return value just results in more code that needs to check for special cases and cases where you can’t actually know which of two states is correct.

Also, there seems to be confusion about my point in comparing it to Smalltalk’s ifTrue:ifFalse:. Of course one should use Ruby idioms when doing Ruby. The ifTrue:ifFalse: example is just a simple and well understood example of having more than one block in your parameter list. I will say that there is a certain kind of semantic elegance that comes from having all your control flow done through methods and objects. Ruby advocates always say that in Ruby “everything is an object”, but it appears that blocks and control flow expressions are not, and in this regard Ruby doesn’t quite live up to expectations set by Smalltalk and LISP.