Back to Basics: Enumerable
This is part of the Back to Basics series.
See the introductory article for an explanation of why this series is important.
To Each Their Own
The word “enumerable” means “able to be counted”. In programming languages, enumerable objects are typically anything that acts as a read-only list.
In the Ruby Standard Library, Enumerable
is a mixin style module based on the method .each
, which is defined by the class or module where this is included.
The simplest .each
method is just a hard-coded set of one or more yield
statements.
class PocketContentGuesses
def each
yield "handses"
yield "knife"
yield "string", "nothing"
end
end
PocketContentGuesses.new.each { |item| p item }
"handses"
"knife"
"string"
This isn’t a terribly common pattern, but it is the heart of more complex implementations.
Notice that there is nothing enforcing the similarity of the objects you yield
(or indeed their count each time, as in the last line), except the ire of those who will try to consume your Enumerable content.
It is generally considered good form to always use the same number and same type of item for each call to
yield
, if you don’t want to get stabbed by irate developers later.
A more conventional each
often delegates to an underlying data structure.
class ArrayWithOrdinals < SimpleDelegator
def each
length.times do |number|
yield [number + 1, self[number]]
end
end
end
ArrayWithOrdinals.new(%w(a b c)).each do |number, item|
puts "##{number}: #{item}"
end
#1: a
#2: b
#3: c
This is still pretty contrived, as any sane person would just delegate to the built in Array#each
method.
class ArrayWithOrdinals < SimpleDelegator
def each
number = 1
__getobj__.each do |item|
yield number, item
number += 1
end
end
end
ArrayWithOrdinals.new(%w(a b c)).each do |number, item|
puts "##{number}: #{item}"
end
#1: a
#2: b
#3: c
There are actually several ways to solve this with other Enumerable methods on Array
, but that would be cheating, no?
A Brief Aside on Enumerators
Now, polite each
methods also return an Enumerator
if no block is given.
An enumerator has an each method as well (which allows chaining of Enumerable functionality), but that each method is implemented as a series of calls to next
, yielding values until a StopIteration
exception is raised.
class ArrayWithOrdinals < SimpleDelegator
def each
return to_enum unless block_given?
number = 1
__getobj__.each do |item|
yield number, item
number += 1
end
end
end
ArrayWithOrdinals.new(%w(a b c)).each.next
# Returns: [1, "a"]
# No output
ArrayWithOrdinals.new(%w(a b c)).each do |number, item|
puts "##{number}: #{item}"
end
#1: a
#2: b
#3: c
Ok, we have our .each
defined, all nice and proper. Now what?
Well, by including Enumerable
, we now have 58 new methods to play with (though several are aliases of each other).
Lets check these out, grouped by function.
We will use simple arrays for most of the examples below, but wherever you see item
you could substitute any yielded value from another each
.
First: Scratching that Each
To read the rest of the series: click here