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!
Responses