Section 5. Blocks

I have described this digital shortcut as a drive down local roads, but now it is time to take to the highways. Highways are fascinating concepts. Although they seem like an inevitable part of the driving infrastructure, highways are actually quite new in the history of roads. As recently as the early 1900s, a drive across the country was essentially the same as driving across town, navigating a dense network of local roads that would get you there eventually but not efficiently. Routes were not clearly demarcated, and road maps as we know them did not exist. Navigation was a challenge when directions comprised only instructions such as “take right at third fork in the road” or “turn left at the willow tree.” Of course, getting lost was inevitable. And accidents and mishaps were all too common on neglected backcountry roads. This might seem a cartoonish depiction of long-distance driving, but that is largely a reflection on just how fast, safe, predictable, and even boring intercity travel has become due to highways. And that is because the highway changed the very nature of long-distance trips.

Highways are built not from asphalt and cement, but from limitations. Instead of connecting to every road, a highway runs largely isolated from local roads, with only a scant selection of exits where new traffic can enter and leave. Highways never actually intersect directly with other roads except by exits and ramps. As a result, local traffic sticks to local roads, and interstate traffic stays on the highways, meaning long-haul truckers do not bumble into suburban cul-de-sacs. Most significant of all, highways simplify navigation for the driver. Instead of drivers having to pick a complex route over local roads, the highway limits their route. To enter a highway is to largely surrender navigational choices: There are no turns or forks in the road to choose; backtracking and U turns are forbidden; your only option is to drive forward until you reach your exit. You can drive faster because you have less to decide, and you can concentrate on driving because you have less to consider. So, what does this have to do with Ruby? Let’s talk about iterators. As long as there have been arrays in computer languages, there has been code that traverses their elements. For the longest time, however, iterating through the elements of an array was a lot like driving cross-country on local roads. Your code (the car, for those slow on metaphors) has to figure out the route. If you are lucky, your code might just need to initialize a pointer to the first element of the array and drive forward to each element by incrementing that pointer’s address, ending before you overshoot the bounds of the array. (Otherwise, you might get fencepost errors, which is a bit like turning at the wrong intersection.) In more-complicated operations such as sorting or binary searching, your travels might involve all sorts of turns and reversals. Better hope you have a good map! Although this is simple, it has never been satisfying. This approach practically breeds bugs: It is easy for callers to make a wrong turn and bumble into memory locations they are not meant to visit—and it also forces callers to worry too much about navigation while driving. There had to be a better way.

When the Gang of Four published their groundbreaking book Design Patterns, they included something in that book called the Iterator pattern. Simply put, an iterator is like a highway for a block of code. The iterator acts by starting the code at the first element (the entrance ramp if you will) and then moving the code forward through each element until it finishes with the last (the exit ramp). The result is much cleaner code. Callers who need to iterate over the element no longer need to worry about keeping track of where they are in the array and how to get to the next element; the iterator handles that. All the calling code needs to do is apply whatever logic it needs against each array element. This process is clearly better, but iterators still have sporadic support in some modern languages. Worse still, many new developers have a weak understanding of how iterators work even in languages that provide them as an option.

As a thoroughly modern language, Ruby uses iterators extensively, but in a way slightly different from existing iterator implementations. In Java, if you want to iterate over a collection, you call a method to get an object that implements the Iterator interface, which abstracts away the internals of the target object. However, you still have to write logic in your loop to drive through the elements of the iterator. The abstraction helps generalize your movement through the array somewhat (you can call a general method such as hasNext(), as opposed to figuring out position < array.size()), but you still have to navigate—which adds a little bit of extra complexity to your code and still makes it possible for you to take a wrong turn somewhere (that is, fencepost errors). Consider this example of code you might write to display a list of comments for a blog posting:

Image

Ruby’s iterators are more comprehensive. Not only do they abstract away the internals of the object, they do the driving through the array for you. Here is the equivalent of the preceding code in Ruby:

Image

Notice that the logic to print each comment does not need to include code to increment to the next position in the array. All you need to provide is a block of code to be applied at each position, and Ruby does the lower-level work of iterating through the object for you. However, even this example code is a bit too simplistic. In Ruby, the preceding command is an example of syntactic sugar provided as a bridge for PHP and Perl programmers. Ruby actually executes that iteration as follows:
@comments.each { |c| puts c.text }

This syntax might seem a little more obscure to express, but it is a lot more powerful to use. To understand how this works, you need to understand blocks. And understanding blocks is the single most important step to understanding Ruby.

Block is a generic computer science term for a chunk of logic, but in Ruby it has a specific meaning. In Ruby, a block is essentially a nameless method: a section of executable code with zero or more input arguments. In Ruby’s syntax, blocks are demarcated either by {} or the keywords do and end. (The usual convention is to use {} for single-line blocks and the keywords for multiline blocks.) The input parameters into a block are surrounded by vertical pipes, as the following examples show:

Image

If the block is the yin of Ruby iteration, yield is the yang. This keyword in Ruby indicates where within the method you want the block to be applied. After the yield statement, you can specify zero or more variables, which are passed in as parameters to the block. This is a bit of an unusual influence. Ruby actually borrowed this feature from CLU, an educational programming language developed at MIT in the late 1970s (and used there by your author in a software engineering course as late as the mid-1990s), which pioneered some early concepts of object-oriented programming but failed to have a major influence on the development of C++ and Java. Blocks are a wonderful feature (as you will see), but their obscure history can make them a bit bewildering to newcomers. The following examples show how yield and blocks work together:

Image

You might have noticed the second example of yield looks strangely familiar. It is the definition we created for each earlier when we needed to get the Enumerable mixin to work. By convention, each is the standard name for forward iterators in Ruby, although you must not think that each is defined differently than other methods; it is possible to create iterators with other names, and it is also possible to define a noniterator method named each if you want (although that would probably confuse your users). You might notice that this implementation of BirdWatcher#each is just calling the each method of the underlying hash and yielding the results upward. Blocks and yields can be chained like this to create higher-level iterators out of lower-level ones (although our example is rather simple). For proof of how effective this is, let’s look more closely at the Enumerable module.

As you saw before, the Enumerable module is a selection of 28 methods that can be mixed into any object that defines an iterator named each. As Listing 4 illustrates, each method in Enumerable could be described as a higher-level iterator, in that they abstract away using the each method within a block-driven call. As an example of how this works, suppose you need to determine whether an array of bird observations contains an endangered species. (In that case, you stop iterating and summon the ornithologists.) In Java, you construct this using a basic Iterator and breaking when you match your condition, as follows:

Image

This should seem straightforward, and you could probably write detection iteration examples like this in your sleep. But why do you need to? All of them are the same. If not found, go to the next element; otherwise, do something and break. What Ruby allows you to do is think at a higher level:
call_scientists if birdlist.any? {|bird| bird.endangered? }

Internally, the any? function works by running over the object’s each method with the block and breaking and returning true if the block’s logic evaluates to true for an element, and false otherwise. That is the genius of Ruby’s block-driven iteration approach. By allowing you to layer iterators above iterators, Ruby enables you to write your logic at the highest level where it makes sense without worrying about wrangling iteration at the lowest level.

Listing 4 Some Examples of Enumerable’s Higher-Level Iterators

Image

Ruby’s block/yield approach is much richer than mere iteration, however. The block approach allows the caller to perform arbitrary calculations against an object, but the target object still maintains control over what the caller can do. This approach also proves useful where the target object has to allow access while maintaining data integrity and properly managing resources. For instance, here is how Ruby uses blocks for reading from files:

Image

In this example, the caller can pass a block (with a file parameter) into the call to File.open. There is no need to call File.close; when this block exits, the File object automatically closes the file. The File object can thus ensure that files are never left in an inconsistent state; even in cases where exceptions are thrown, the file is safely closed and handles released. If you can recall any code where you forgot to close a file on an error return and leaked handles as a result, the advantage of this approach should be obvious. Similarly, you can also use blocks for other limited resources that need to be used safely: locks, database connections, and server sockets, among others.

Blocks also prove useful when it is easier to tackle a problem as nested parts rather than as a complicated whole. The Builder library used in Rails’s .rxml files to create XML is a prime example of this approach. XML has proven to be a popular format for data exchange with its human-readable, hierarchical structure; but XML contains many subtle gotchas that can stump developers who just need to output a file. The Builder class for Rails solves this issue by abstracting the process of building XML files as a series of nested blocks, with elements posing as methods and attributes as parameters:

Image

This example (simplified from code in Typo) builds a syndication file in the Atom feed format. The XML that results from this call might look like this:

Image

In this example, Builder mirrors the hierarchical structure of the result XML with a hierarchical nesting of blocks. (In this example, we also call a partial for items, which renders each item in the feed.) Blocks are prime examples of the concise power of Rubyisms in action.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset