Skip to content
academia.sh

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

  • Enumerable is a module that gives every class defining each methods such as map, sort, min_by, group_by and tally.
  • This lesson’s responder is the module: the (1..5).each_slice(2) message is answered by Enumerable, not by Range.
  • Adding include Enumerable and a single each to your own class is enough; the owner of map turns out to be the module and the owner of each the class.
  • The bounding measurement: the same name comes from a different owner depending on the receiver; sum comes from the module on a range and from Array on 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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close