Yield or Enumerate
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
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
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!