Changing the Way an Object Iterates

Problem

You want to use a data structure as an Enumerable, but the object's implementation of #each doesn't iterate the way you want. Since all of Enumerable's methods are based on each, this makes them all useless to you.

Discussion

Here's a concrete example: a simple array.

array = %w{bob loves alice} array.collect { |x| x.capitalize } # => ["Bob", "Loves", "Alice"]

Suppose we want to call collect on this array, but we don't want collect to use each: we want it to use reverse_each. Something like this hypothetical collect_reverse method:

array.collect_reverse { |x| x.capitalize } # => ["Alice", "Loves", "Bob"]

Actually defining a collect_reverse method would add significant new code and only solve part of the problem. We could overwrite the array's each implementation with a singleton method that calls reverse_each, but that's hacky and it would surely have undesired side effects.

Fortunately, there's an elegant solution with no side effects: wrap the object in an Enumerator. This gives you a new object that acts like the old object would if you'd swapped out its each method:

require 'enumerator' reversed_array = array.to_enum(:reverse_each) reversed_array.collect { |x| x.capitalize } # => ["Alice", "Loves", "Bob"] reversed_array.each_with_index do |x, i| puts %{#{i}=>"#{x}"} end # 0=>"alice" # 1=>"loves" # 2=>"bob"

Note that you can't use the Enumerator for our array as though it were the actual array. Only the methods of Enumerable are supported:

reversed_array[0] # NoMethodError: undefined method '[]' for #

 

Discussion

Whenever you're tempted to reimplement one of the methods of Enumerable, try using an Enumerator instead. It's like modifying an object's each method, but it doesn't affect the original object.

This can save you a lot of work. Suppose you have a tree data structure that provides three different iteration styles: each_prefix, each_postfix, and each_infix. Rather than implementing the methods of Enumerable for all three iteration styles, you can let each_prefix be the default implementation of each, and call tree.to_enum(:each_postfix) or tree.to_enum(:each_infix) if you need an Enumerable that acts differently.

A single underlying object can have multiple Enumerable objects. Here's a second Enumerable for our simple array, in which each acts like each_with_index does for the original array:

array_with_index = array.enum_with_index array_with_index.each do |x, i| puts %{#{i}=>"#{x}"} end # 0=>"bob" # 1=>"loves" # 2=>"alice" array_with_index.each_with_index do |x, i| puts %{#{i}=>#{x.inspect}} end # 0=>["bob", 0] # 1=>["loves", 1] # 2=>["alice", 2]

When you require 'enumerator', Enumerable sprouts two extra enumeration methods, each_cons and each_slice. These make it easy to iterate over a data structure in chunks. An example is the best way to show what they do:

sentence = %w{Well, now I've seen everything!} two_word_window = sentence.to_enum(:each_cons, 2) two_word_window.each { |x| puts x.inspect } # ["Well,", "now"] # ["now", "I've"] # ["I've", "seen"] # ["seen", "everything!"] two_words_at_a_time = sentence.to_enum(:each_slice, 2) two_words_at_a_time.each { |x| puts x.inspect } # ["Well,", "now"] # ["I've", "seen"] # ["everything!"]

Note how any arguments passed into to_enum are passed along as arguments to the iteration method itself.

In Ruby 1.9, the Enumerable::Enumerator class is part of the Ruby core; you don't need the require statement. Also, each_cons and each_slice are built-in methods of Enumerable.

See Also

Категории