Lesson 09 / 12
The `Enumerable` Module
The iteration methods an array does not define itself are measured as coming from the Enumerable module, your own class is given the same ability with a single each, and the module is shown not to take over every job.
Published Updated
In this lesson
- Measures the owner of Enumerable methods with owner
- Gives your own class iteration methods with include Enumerable and a single each
- Reads the results of sort_by, group_by, min_by and tally
- Measures that a method of the same name can have different owners on an array, a range and a module
Contents
The previous lesson showed that map on a hash is defined not in Hash but in the Enumerable module. This lesson looks at that module. A module is a collection of methods that is mixed into (include) a class; it does not produce objects by itself. M08 built the object-oriented foundations; here only the messages this module answers are measured.
This lesson’s responder is the module. Arrays, ranges and hashes are different classes, but a shared module answers many of their iteration messages.
The Messages the Module Answers
The first measurement sends the each_slice message to a range. The core classifies the result by looking at the method’s owner: if the owner is a Class, the responder is a class; otherwise it is a module.
# reply.rb — helpers that classify who answers a message
# Classification asks in order: is the expression a message at all (defined?),
# then do two different blocks give two different results, then who owns the method.
def responder(definition, owner: nil, result: nil, with_blocks: nil)
return ["syntax", "defined? #{definition}"] unless definition == "method"
if with_blocks
first, second = with_blocks.call
return ["block", "#{first.inspect} / #{second.inspect}"] unless first == second
end
kind = owner.instance_of?(Class) ? "class" : "module"
[kind, "#{owner}, result #{result.inspect}"]
end
# slice.rb — who answers the each_slice message
require_relative "reply"
range = (1..5)
owner = range.method(:each_slice).owner
result = range.each_slice(2).to_a
kind, evidence = responder(defined?(range.each_slice(2)), owner: owner, result: result)
puts "#{kind} #{evidence}"
module Enumerable, result [[1, 2], [3, 4], [5]]
This is the core’s slicing observation: the responder is a module, and the owner is Enumerable. The Range class does not define each_slice itself; it is enough for a range to know how to hand out its elements with each. The module’s other methods are in this lesson’s second measurement.
# tools.rb — Enumerable methods
fruits = %w[plum peach grape fig]
puts fruits.sort_by { |f| f.size }.first
puts fruits.group_by { |f| f.size }.inspect
puts fruits.min_by { |f| f.size }
puts %w[a b a].tally.inspect
puts (1..6).select(&:even?).inspect
fig
{4 => ["plum"], 5 => ["peach", "grape"], 3 => ["fig"]}
fig
{"a" => 2, "b" => 1}
[2, 4, 6]
sort_by put the shortest name first, group_by built a hash that groups the names by length, and min_by gave the shortest name. tally counted how many times each element appears. select(&:even?) picked the even numbers from the range.
Mixing It into Your Own Class
The module’s ability becomes a class’s ability only once it is mixed into the class. The smallest form: the class defines only each and says include Enumerable. Class design is the subject of M08; here the class is built in its smallest form just for the measurement.
# numbers.rb — mixing Enumerable into our own class
class Numbers
include Enumerable
def each
yield 3
yield 1
yield 2
end
end
numbers = Numbers.new
puts numbers.map { |n| n * 2 }.inspect
puts numbers.sort.inspect
puts numbers.min_by { |n| -n }
puts "map=#{numbers.method(:map).owner} each=#{numbers.method(:each).owner}"
puts numbers.to_a.inspect
puts numbers.include?(2)
puts numbers.first
puts numbers.reduce(:+)
puts numbers.each_with_index.to_a.inspect
puts numbers.is_a?(Enumerable)
[6, 2, 4] [1, 2, 3] 3 map=Enumerable each=Numbers [3, 1, 2] true 3 6 [[3, 0], [1, 1], [2, 2]] true
The class wrote only each; map, sort and min_by arrived without any extra definition. min_by { -n } gave the largest number, because the one with the smallest negative value is 3. The fourth line shows the sharing: the owner of map is Enumerable, and the owner of each is the Numbers class. to_a collected the elements in the order each yielded them: [3, 1, 2]. include?, first and reduce also rested on the same single each; reduce(:+) summed the elements and gave 6. each_with_index attached an index to each element. The last line confirms that instances of the class are recognized as Enumerable. The class did not define <=>, because sort here compares numbers with the numbers’ own comparison; comparing the class’s instances with each other is a separate job. CB8. This class yields three values with each; if each produced an endless sequence, methods that read everything, such as sort, would never finish, but that behavior is not measured here.
The Types the Module Returns
The module’s methods return particular types regardless of the receiver’s type. This is a contract the module gives its users: when you send the same message to a range, an array or your own class, you know the result type without looking at the receiver.
# types.rb — types returned by Enumerable methods
range = (1..3)
puts range.map { |n| n }.class
puts range.select { |n| n > 1 }.class
puts range.group_by { |n| n.odd? }.class
puts range.tally.class
puts range.each_slice(2).class
puts range.min_by { |n| -n }.class
Array Array Hash Hash Enumerator Integer
map and select returned arrays, group_by and tally returned hashes; each_slice without a block gave an iterator, and min_by gave a single element. The range was the receiver here, but the result types depend on the module’s definition, not on the range. The Hash#select example in the Arrays and Hashes lesson shows the limit of this: there the class’s own definition had returned a hash. CB9. This table was measured only with a range receiver; that the same methods return the same types on your own class has been seen for map in the Numbers measurement above, and is not assumed for the others.
Chained Methods
The real strength of Enumerable is that small methods connect to each other. The measurement below runs five methods on a range and prints the owner of each next to it.
# chain.rb — the result and owner of each method
s = (1..4)
puts "each_with_object=#{s.each_with_object([]) { |n, a| a << n * n }.inspect} owner=#{s.method(:each_with_object).owner}"
puts "inject=#{s.inject(:+)} reduce=#{s.reduce(1) { |a, n| a * n }} owner=#{s.method(:inject).owner}"
puts "zip=#{s.zip(%w[a b c d]).inspect} owner=#{s.method(:zip).owner}"
puts "partition=#{s.partition(&:even?).inspect} owner=#{s.method(:partition).owner}"
puts "each_cons=#{s.each_cons(2).to_a.inspect} owner=#{s.method(:each_cons).owner}"
each_with_object=[1, 4, 9, 16] owner=Enumerable inject=10 reduce=24 owner=Enumerable zip=[[1, "a"], [2, "b"], [3, "c"], [4, "d"]] owner=Enumerable partition=[[2, 4], [1, 3]] owner=Enumerable each_cons=[[1, 2], [2, 3], [3, 4]] owner=Enumerable
each_with_object carried an empty array along and collected the squares. inject(:+) gave the sum and reduce(1) { ... } gave the product; the owner of inject was printed as Enumerable. zip paired two sequences, partition split the elements into two arrays by whether the block was true, and each_cons(2) gave consecutive pairs. In all five, the printed owner was Enumerable: even though Range defines its own each, the responder for this chain is the module. CB27. The owner measurement was made only with a Range receiver; the owner of reduce was not printed separately.
The Lazy Chain
A lazy chain does not run immediately; it produces only as many elements as the result asks for. That is why select and first(3) can finish on an endless range. with_index attaches an index to the elements, and the first index can be given.
# lazy.rb — a lazy chain and with_index
puts (1..Float::INFINITY).lazy.select(&:even?).first(3).inspect
puts (1..3).lazy.class
puts "lazy owner=#{(1..3).method(:lazy).owner}"
puts %w[a b].each.with_index(1).to_a.inspect
puts %w[a b].map.with_index { |h, i| "#{i}#{h}" }.inspect
[2, 4, 6] Enumerator::Lazy lazy owner=Enumerable [["a", 1], ["b", 2]] ["0a", "1b"]
On the endless range, first(3) stopped after finding the first three even numbers and gave [2, 4, 6]. Enumerable answered the lazy message, and the class of the returned object was Enumerator::Lazy. each.with_index(1) started the index at one; map.with_index handed the index to the block along with the element and produced the transformed array. CB28. Only these three chains were measured.
An External Iterator
each called without a block returns an iterator; next advances it by hand. When the elements run out, StopIteration is raised.
# external_iterator.rb — advancing by hand with next e = [1, 2, 3].each puts e.next puts e.next puts e.next result = begin e.next rescue StopIteration "done" end puts result puts e.size
1 2 3 done 3
Three next calls gave the elements in order. The fourth call raised StopIteration, rescue caught it and done was printed. size returned 3. CB29. size was measured only in this example; iterators whose size is not known in advance were not measured.
Methods Coming from the Mixin
When your own class defines only each and mixes in Enumerable, it gains dozens of methods. The Box class below keeps its elements in an array and delegates to the array’s each inside its own each. Then nine different methods are called.
# box.rb — a class that defines only each
class Box
include Enumerable
def initialize(*items)
@items = items
end
def each(&block)
@items.each(&block)
self
end
end
k = Box.new(3, 1, 2)
puts "sort=#{k.sort.inspect} owner=#{k.method(:sort).owner}"
puts "min=#{k.min} max=#{k.max} sum=#{k.sum}"
puts "map=#{k.map { |n| n * 2 }.inspect}"
puts "each_slice=#{k.each_slice(2).to_a.inspect}"
puts "first=#{k.first} count=#{k.count} sort_by=#{k.sort_by { |n| -n }.inspect}"
puts "each owner=#{k.method(:each).owner}"
sort=[1, 2, 3] owner=Enumerable min=1 max=3 sum=6 map=[6, 2, 4] each_slice=[[3, 1], [2]] first=3 count=3 sort_by=[3, 2, 1] each owner=Box
Box defined only each; the owner of sort was printed as Enumerable. min, max, sum, map, each_slice, first, count and sort_by all worked through each and gave the expected results. The responder is the module; the owner of each is Box. The module learns how to reach the elements from the class. CB42. Only these nine methods were tried; it is not assumed that every method of the mixin works.
Without an each
The module does not provide each itself; the class must define it. A class that does not define each accepts the mixin, but the methods do not work.
# empty.rb — a class that does not define each
class Empty
include Enumerable
end
result = begin
Empty.new.map { |x| x }
rescue NoMethodError => error
error.class
end
puts result
puts Empty.new.respond_to?(:each)
NoMethodError false
The Empty class mixed in Enumerable but did not define each. The map call raised NoMethodError and respond_to?(:each) gave false. The contract runs one way: Enumerable methods assume that the class’s each method can be called. CB43. The error message was not printed; only its class was measured.
Query Methods and Their Owners
any?, all?, none?, find and take_while query the collection. On an array, do all of these methods come from Enumerable? The owner line measures this.
# queries.rb — query methods and their owners
s = [1, 2, 3, 4]
puts "any=#{s.any? { |n| n > 3 }} all=#{s.all?(Integer)} none=#{s.none?(&:zero?)}"
puts "find=#{s.find { |n| n > 2 }} take_while=#{s.take_while { |n| n < 3 }.inspect}"
puts "owner=#{s.method(:take_while).owner} #{s.method(:find).owner} #{s.method(:any?).owner}"
any=true all=true none=true find=3 take_while=[1, 2] owner=Array Array Array
The query results are as expected: any?, all?(Integer) and none?(&:zero?) were true, find gave 3 and take_while gave [1, 2]. The owner of all three methods was printed as Array: the array defines these methods itself instead of leaving them to Enumerable. In the previous section the owner of sort for Box was Enumerable; so the responder depends on the receiver’s class. CB44. The measurement was made on Ruby 4.0.7 and only with an Array receiver.
Other Methods from the Mixin
The methods that come with the mixin are not limited to summing and sorting. minmax, which gives the smallest and the largest together, min(2), which gives the first n smallest, each_with_index, which passes the index along, lazy chains and reverse traversal all rest on each too. The Shelf class below again defines only each.
# shelf.rb — a second class that defines each, and more methods
class Shelf
include Enumerable
def initialize(*items)
@items = items
end
def each(&block) = @items.each(&block)
end
r = Shelf.new(4, 2, 9)
puts r.minmax.inspect
puts r.min(2).inspect
puts r.each_with_index.map { |n, i| n * i }.inspect
puts r.lazy.map { |n| n + 1 }.first(2).inspect
puts r.entries.inspect
puts r.reverse_each.to_a.inspect
[2, 9] [2, 4] [0, 2, 18] [5, 3] [4, 2, 9] [9, 2, 4]
minmax gave [2, 9] and min(2) gave [2, 4]. each_with_index.map multiplied each element by its index: [0, 2, 18]. lazy.map { ... }.first(2) transformed only the first two elements: [5, 3]. entries gave the elements in insertion order and reverse_each in reverse. In none of the six calls did Shelf define a new method; all derived from each. CB52. The owners of these methods were not printed; only their results were measured.
Comparable for Sorting
sort compares elements with <=>. Objects that do not know this comparison cannot be sorted. A class that defines <=> and mixes in Comparable gains sort, min and operators such as <.
# compare.rb — comparable and sort
class Thing; end
result = begin
[Thing.new, Thing.new].sort
rescue ArgumentError => error
error.class
end
puts result
class Label
include Comparable
attr_reader :length
def initialize(length)
@length = length
end
def <=>(other) = length <=> other.length
end
puts [Label.new(3), Label.new(1)].sort.map(&:length).inspect
puts [Label.new(3), Label.new(1)].min.length
puts Label.new(1) < Label.new(2)
puts "owner=#{Label.new(1).method(:<).owner}"
ArgumentError [1, 3] 1 true owner=Comparable
Two Thing objects without a <=> definition could not be sorted: sort raised ArgumentError. Label defined only <=> and mixed in Comparable; sort gave [1, 3] and min gave 1. The < operator worked too, and the owner of < was printed as Comparable. The module answers the operator; the class only supplied the comparison rule. CB53. The measurement was made with a single field.
The Bounding Measurement
The module does not take over every job. A method of the same name can come from a different owner on different receivers: if the class wrote its own definition, that definition answers.
# owners.rb — the same name, different owners
puts "(1..3).sum #{(1..3).method(:sum).owner}"
puts "[3, 1].sum #{[3, 1].method(:sum).owner}"
puts "[1].sort_by #{[1].method(:sort_by).owner}"
puts "[3, 1].sort #{[3, 1].method(:sort).owner}"
puts "[3, 1].min #{[3, 1].method(:min).owner}"
puts "(1..3).min #{(1..3).method(:min).owner}"
(1..3).sum Enumerable [3, 1].sum Array [1].sort_by Enumerable [3, 1].sort Array [3, 1].min Array (1..3).min Range
sum comes from Enumerable on a range and from Array on an array. sort_by belongs to Enumerable on an array too, but sort and min are Array‘s own methods on an array. For a range, min is Range‘s own method. The results belong to Ruby 4.0.7. The six lines gave three different owners: Enumerable, Array and Range. The names sum and min work on both receivers and the caller sees no difference, but a class or a module answers the message depending on the receiver. So the owner question starts from the receiver’s type, not from the method’s name. CB10. None of the six lines compares the value of the result; only the owner is asked, so this measurement does not say whether the module and the class give the same result. In other words, the same name is the module’s answer or the class’s answer depending on the receiver; the reason for this is the subject of a later course and is not explained here.
Summary
Enumerableis a module that gives every class definingeachmethods such asmap,sort,min_by,group_byandtally.- This lesson’s responder is the module: the
(1..5).each_slice(2)message is answered byEnumerable, not byRange. - Adding
include Enumerableand a singleeachto your own class is enough; the owner ofmapturns out to be the module and the owner ofeachthe class. - The bounding measurement: the same name comes from a different owner depending on the receiver;
sumcomes from the module on a range and fromArrayon an array.
Next Step
In this lesson a method’s body called the block with yield. The next lesson turns the block into an object: Proc and lambda are two ways of keeping the same code in a variable, and the owner of the call message is the same for both.
To keep your progress and take notes, Log in
My notes
Log in to take notes.