blogstrapping

How Do I Love Thee, Ruby Blocks?

Some people complain about the block syntax in Ruby for callbacks and closures, which litters the language's standard library, particularly in the case of iterators. I happen to find the block syntax incredibly convenient, and in many cases it provides some significant benefits over the more general lambda construct (which Ruby also offers). I believe examining the matter via examples can prove illuminating. The first example uses a lambda (or "anonymous subroutine" in the parlance of the example language) in Perl, the language in which I first encountered the concept of lexical closures; the second uses a Ruby block, in (of course) Ruby.

As an aside, I should point out that the reason the $iterator assignment is pressed up against the underside of the Perl example's generator procedure is my desire to group the infrastructure together and separate the case where the iterator object itself executes. This should, I hope, more clearly indicate what is happening where to a casual glance by the reader.

Perl

sub generate {
  my $base = $_[0];
  my $iterations = $_[1];
  my $i = $base;
  sub {
    while ($i < ($base + $iterations)) {
      print "$i\n";
      $i += 1;
    }
  }
}
$iterator = generate(5, 5);

$iterator->();

Ruby

def iterator(base, iterations)
  base.upto(base + iterations - 1) do |i|
    yield i
  end
end

iterator(5,5) {|i| puts i }

Note

I must have been suffering some serious lack of sleep when I originally wrote this, because my Ruby code lacked some idiomatic style, and both my Perl code and my Ruby code contained a gross syntactic oversight. Both problems have since been rectified.

Explanation

A Ruby "block" is essentially a special syntax for callbacks complete with added sugar for closure arguments that make it absurdly convenient to create a generalized closure generator that you will not even see in your code, where the generated closures are particularly suitable for one-off operations conforming to a pattern established in the generator. Notice how the canonical closure example pattern in Perl explicitly defines all closure behavior within the generator, while Ruby's block/yield pattern allows for one-off behavior patterns to be defined as instances of a more general pattern.

In both cases, the more general pattern is "do something with every number from the base number up to, but not including, the iterations number", and the one-off pattern is "print each result". In Perl, the easy way to implement this one-off pattern is to tie it to the general pattern within the generator definition. In Ruby, you could just as easily do that, but it would make the generator useless for other use cases with different on-off operational patterns, and it is just as easy to implement the one-off print pattern when calling the generator rather than when defining it. It is generally considered better practice to separate side-effect code (like printing) from functional code (taking arguments, and returning values, without side-effects), for good reasons related to more comprehensible and maintainable code.

The end result is that Ruby provides a kind of "inside-out" approach to defining callbacks (and, as a sub-case that is more common in practice, closures) that offers very convenient generalization of the generator definition.

Incidentally to the approaches used in these examples, the Perl example also creates a home for variables to persist long after they are used, while the Ruby example cleans up after itself once it has performed its task. In a trivial case like this, that is not a tremendous concern, but there are times when a closure that automagically vanishes along with all the state that persists within it is a tremendous boon for resource management.

There is a downside to the Ruby block/yield pattern being a part of the language, in that it complicates the language the way all syntactic sugar complicates a language, of course. Some people I have encountered find the fact there are several different abstractions for lambda-like procedure definitions (yes, Ruby has more than just the bare lambda and the block) rather confusing. The ample opportunities to clarify code, generalize callback designs (even if you only count the iterator closures), and cut down on object persistence beyond when the relevant objects are actually used, make the Ruby block a pretty obvious net win as an addition to the language.

Final Note

I find Ruby quite a fun and productive language to use, and it is the language I use most these days. I used to like Perl as much as I like Ruby now, but my appreciation for it has waned somewhat after spending several years away from it then coming back to it briefly a couple of years ago, once I saw the problems with Perl with fresh eyes. I think I am avoiding that problem with Ruby; I see that there are many issues with the language, as there are with every language of course. That does not mean my enjoyment of Ruby will never wane, but it does mean that I will probably not suddenly find myself wondering how I overlooked a lot of problems at some point in the future.

For those who have not considered the Ruby block construct in enough detail to recognize its benefits, I hope this helps them understand that there are in fact significant benefits to be had. It is not just some pointless extra feature bolted onto the language, as some have suggested. While all the benefits except code clarity can be had by the addition of (relatively) large quantities of code with the use of lambdas, code clarity is a substantial reward for the addition of some syntactic sugar, and that alone makes Ruby blocks worthwhile -- and I think many Python afficionados who express distaste for the multifarious lambda syntactic forms in Ruby would express similar appreciation and justification for Python's generators (ignoring for the moment the fact that lambdas have been essentially neutered for most of Python's life, though I have encountered rumors that has changed or will change soon, as of this writing).

The upshot is that that no claim is being made that Ruby is the best language in the world because of Ruby blocks. Rather, Ruby blocks are simply nice things to have, and constitute one of many reasons I like the language. While your appreciation for Ruby may be much less positive than mine, I hope I have at least made clear the fact that there is something good to be had from Ruby's blocks. While I would not use Ruby for everything, they, along with many other features of the language, help me enjoy programming in Ruby in those cases where I do find it to be a suitable choice of implementation language.