Episode #064: Yield or Enumerate

Upgrade to download episode video.

Episode Script

I hope you're not tired of hearing about Enumerators, because I'm not quite done talking about them.

Many of Ruby's collection iteration methods have an interesting trick: if we call them without a block, they return an Enumerator. For instance, take the #each_slice method. If we call it with a block, it iterates through the collection, yielding slices of the collection until it reaches the end.

require 'pp'
[0,1,2,3,4,5,6,7,8,9].each_slice(2) do |slice|
  pp slice
end
# >> [0, 1]
# >> [2, 3]
# >> [4, 5]
# >> [6, 7]
# >> [8, 9]

But if we call it without a block, it returns an Enumerator:

require 'pp'
[1,2,3,4,5,6,7,8,9].each_slice(2) # => #<Enumerator: [1, 2, 3, 4, 5, 6, 7, 8, 9]:each_slice(2)>  

This is convenient for chaining enumerable operations.

require 'pp'
sums = [0,1,2,3,4,5,6,7,8,9].each_slice(2).map do |slice|
  slice.reduce(:+)
end
sums # => [1, 5, 9, 13, 17]

Any method that yields a series of values could potentially be a lot more flexible if it behaved like this, returning an Enumerator in the absence of a block. So we might reasonably want to know how to duplicate this behavior in our own methods.

As it happens, it's not hard at all. In fact, it's a one-liner.

def names
  return to_enum(:names) unless block_given?
  yield "Ylva"
  yield "Brighid"
  yield "Shifra"
  yield "Yesamin"
end

This line checks to see if a block has been provided. If so, it allows the method to continue normally. Otherwise, it constructs an Enumerator for the current method and immediately returns it. When we try it out, we can see that calling the method without a block returns a fully-functional Enumerator.

names                           # => #<Enumerator: main:names>
names.to_a                      # => ["Ylva", "Brighid", "Shifra", "Yesamin"]

One potential improvement we can make to this line is to replace the name of the method with the __callee__ special variable. This variable always contains the name of the current method. By making this change, we eliminate the duplication of the method name, and ensure that if we ever change the name of the method the call to #to_enum will continue to work.

def names
  return to_enum(__callee__) unless block_given?
  yield "Ylva"
  yield "Brighid"
  yield "Shifra"
  yield "Yesamin"
end

That's all for today. Happy hacking!