One of my favorite features in Ruby is the Enumerable module and the ability to create enumerators.
For background, an enumerator in Ruby is like an iterator, it responds to the method next which returns successive elements from some container that
is often an array or hash. Ruby’s Enumerable module contains many helper methods for working with
enumerators such as
any?, and many more. These methods also return an enumerator which allows methods to be chained.
As an example, say I have an array of objects we’ll call users that each have a name. If I want to transform that array into a new array where each object
becomes an array of the letters in the name string, I can do the following:
What happens though when you do something like:
This happens because Ruby doesn’t return from map until it runs out of items in the enumerable you call map on, in this case the range 1 to infinity.
So how might we get this to work if we know how many numbers we want to process? Well we can change
#map to be lazy by changing the example to:
This then returns the first 10 calls to our lazy enumerator. The call to
#to_a is necessary because once you add
#lazy, all returned enumerators are
also lazy and
#to_a triggers the evaluation of the enumerator to return a result. This allows us to write some pretty powerful transformations but only
do the computation that we actually need.
Another cool feature of Ruby is what you can do with Ruby’s
yield keyword. In a few projects now I have created an interfacing for dealing with iteration
that is more complicated than just stepping through each item one-at-a-time. I recently wrote an example as part of an implementation for Conway’s Game of Life.
I wrote a method that would iterate through each cell in the board and yield the cell along with the number of live neighbors for that cell. I would also set
the new value of the cell to the return value of the block. This made it really simple to implement the rules for the game. Here’s what that looks like:
Here is the implementation for
#duplicate_board does what you would expect, it returns a copy of the existing board of cells. Next, we step through each cell in the existing board. This is
another use of
yield to make a nice interface.
#each_cell_with_location yields the value of each cell along with its location on the board. This allows us
to count the number of live (true) cells using the
#neighbors method which returns an enumerator so we can use the Enumerable
#count method. Then we yield
the value of the cell and the number of live neighbors and store the result in the new board at the appropriate location. Finally we return the new board for use
in creating a new copy of the game board.
There are several functions more that make this work, too many to show here but if you’re interested you can see the implementation on Github.
I really like the interface you get for these methods by using
yield. As seen above, it allows you to easily add layers of abstraction. In the
I am given the number of live neighbors so that in that method I don’t need to worry about computing that and instead I can stick with the single responsibility of
deciding what is the next state of the cell I am checking.
I actually have an even more complicated example that helps show off the power of using yield and enumerators like this but it is too large to copy into a blog post. Here is a quick summary though. This example is from a game combat simulator. To explain what the pieces represent requires a little bit of background on the rules of the game.
Many units in the game consist of a number of models (as in miniatures) that are aligned in a grid of ranks and files. To represent this, I effectively used a multi-dimensional array to track the location and what model was in each position. Each rank must contain exactly as many files as the rank in front of it with the sole exception of the most rear rank. The rules of the game don’t specify where the models in this last rank must be located so for this simulator, I used the Strategy Pattern (from the Gang of Four book).
To start it off I created three strategies to choose from: center alignment which tries to center the last rank, left alignment which pushes all models to the left side, and right alignment which pushes all models to the right side. Given that I know how wide (how many files) the unit is and which strategy is being used, there is a single position which represents the next place to add a model to the unit while observing the rules for the formation. Because of that, I could create a method that would yield the rank and file numbers for that next position successively until you no longer wanted to add more models. Check out this gist for what that looks like. I only included the implementation of the Left Alignment Strategy for brevity.