Enumerable
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18); features newer than Ruby 3.0 name the version that
introduced them. Enumerable is the module behind most of what makes Ruby collections pleasant: a class that defines
each and includes Enumerable receives sixty-one more methods, from map and select to group_by, each_slice,
sum, min_by, tally and lazy, without writing any of them. Array, Hash, Range, Struct, Set, Dir, IO
and Enumerator all use it, and so does the shelf Catalog built in this reference. This page explains the contract
between each and the module, shows every method family with output that was produced by running the code, covers the
points where block parameters and return values surprise people, and explains why an iterator is almost always a better
choice than a for or while loop.
Enumerators that wrap each (external iteration with next, Enumerator.new, Enumerator::Lazy, Enumerator::Chain,
Enumerator.produce and ArithmeticSequence) are the subject of
Enumerators and Lazy Evaluation; this page only shows
how a block-less call returns one. Blocks themselves, yield, break, next and the it parameter are described in
Blocks, Procs and Lambdas, the loop and case syntax in
Control Expressions, and include and the ancestor chain in
Modules and Mixins. The collection classes are in
Arrays, Hashes,
Ranges, Sets and
Struct, Data and Value Objects. The <=> operator that
sort, min and max depend on is covered in
Equality, Hashing and Comparison, and
case/in over the results in Pattern Matching. For the same ideas
in other languages, see Rust iterators and
Go iterators and range-over-func.
Every example was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 and linted with RuboCop 1.92 using the
configuration of the running shelf gem (target Ruby 4.0, double-quoted strings, NewCops: enable). The values after
# => are the real results, written in the Ruby 3.4+ inspect format ({"a" => 1}, {name: "Ico"}), and a comment
starting with # prints: or # | is real standard output. Where an example deliberately does something a cop dislikes
(a for loop, a _1 block parameter, a block that yields two values, an accumulator that is not returned), the file says
so with a rubocop:disable comment; the section on RuboCop cops explains each one. The
examples that use the shelf gem are run with ruby -Ilib (see the shelf gem source).
The module and the each protocol
Enumerable is an ordinary module with one requirement: the object that includes it must define each, and each must
yield the elements one at a time. Almost every other method is written in terms of each: map calls each and collects
what the block returns, first(3) calls each and stops after the third element, include? calls each and stops at the
first match. The module does not know or care how the elements are produced. They may live in an array, be read from a
file, come from a database cursor or be computed on demand, and the same sixty-one methods work on all of them.
The methods that rank or compare elements (sort, min, max, minmax and their _by variants without a custom block)
additionally need the elements to respond to <=>; that is a requirement on the elements, not on the collection, and it
is described under Sorting and extremes.
The next file uses the real shelf Catalog, which is a class with an each that iterates over a snapshot of its store
(taken while holding a Thread::Mutex) and an include Enumerable. Note that it is a collection of Book values, and that
no map, sort_by, count, first or include? is defined in the library: they all come from the module. Catalog does
not respond to size either, because Enumerable has no size or length; count walks the elements.
# frozen_string_literal: true
require "shelf"
def book(isbn, title, author) = Shelf::Book.new(Shelf::Isbn.new(isbn), title, Shelf::Author.new(author))
catalog = Shelf::Catalog.new
catalog.add(book("978-0-306-40615-7", "Ruby Basics", "A. Writer"))
.add(book("978-1-234-56789-7", "Iterators in Depth", "B. Coder"))
.add(book("978-9-876-54321-7", "Blocks and Procs", "A. Writer"))
Shelf::Catalog.include?(Enumerable) # => true
Shelf::Catalog.ancestors.first(3) # => [Shelf::Catalog, Enumerable, Object]
catalog.map(&:title) # => ["Ruby Basics", "Iterators in Depth", "Blocks and Procs"]
catalog.sort_by(&:title).map(&:title) # => ["Blocks and Procs", "Iterators in Depth", "Ruby Basics"]
catalog.count # => 3
catalog.first.title # => "Ruby Basics"
catalog.first(2).map(&:title) # => ["Ruby Basics", "Iterators in Depth"]
catalog.min_by { |b| b.title.size }.title # => "Ruby Basics"
catalog.include?(book("978-0-306-40615-7", "Ruby Basics", "A. Writer")) # => true
catalog.group_by { |b| b.author.name }.transform_values(&:size) # => {"A. Writer" => 2, "B. Coder" => 1}
catalog.each.class # => Enumerator
catalog.respond_to?(:size) # => false
Ruby 4.0 documents these as the core classes that include (or, for ENV, extend) Enumerable: ARGF, Array, Dir,
Enumerator, ENV, Hash, IO, Range and Struct, plus Set (a core class since Ruby 4.0) and, from the bundled
csv gem, CSV, CSV::Table and CSV::Row. Data is deliberately not on the list: a Data object is
not a collection of its members (see Hash, Range, Struct and Set as
enumerables).
Everything the module provides
Enumerable.instance_methods(false) lists sixty-one methods in Ruby 4.0 (the same count as in 3.4). The next file prints
them, and shows how many of them each core class replaces with its own, usually faster, implementation. The semantics of an
override are meant to be identical, with small documented exceptions that are called out below.
# frozen_string_literal: true
methods = Enumerable.instance_methods(false).sort
methods.size # => 61
puts(methods.each_slice(8).map { |row| row.join(" ") })
# | all? any? chain chunk chunk_while collect collect_concat compact
# | count cycle detect drop drop_while each_cons each_entry each_slice
# | each_with_index each_with_object entries filter filter_map find find_all find_index
# | first flat_map grep grep_v group_by include? inject lazy
# | map max max_by member? min min_by minmax minmax_by
# | none? one? partition reduce reject reverse_each select slice_after
# | slice_before slice_when sort sort_by sum take take_while tally
# | to_a to_h to_set uniq zip
# Array, Hash, Range, Struct and Set override some of these.
overrides = [Array, Hash, Range, Struct, Set].to_h do |klass|
[klass, methods.count { |m| klass.instance_method(m).owner == klass }]
end
overrides.to_h # => {Array => 31, Hash => 9, Range => 11, Struct => 4, Set => 4}
# Enumerable has no size or length; count walks the elements.
Enumerable.instance_methods.intersect?(%i[size length]) # => false
Enumerable.method_defined?(:count) # => true
# The mixin only needs each, which the including class supplies.
Enumerable.method_defined?(:each, false) # => false
Enumerable.instance_method(:sort).arity # => 0
Comparable.instance_methods.sort # => [:<, :<=, :==, :>, :>=, :between?, :clamp]
| Family | Methods | Return value |
|---|---|---|
Querying |
|
Booleans, an Integer or a Hash |
Fetching |
|
Elements or Arrays |
Searching and filtering |
|
An element, an index or an Array |
Transforming |
|
A new Array, Hash or Set |
Reducing |
|
A single value |
Sorting |
|
A new Array |
Grouping and slicing |
|
A Hash, Arrays or an Enumerator |
Iterating |
|
The receiver, the memo object or |
Lazy and chained |
|
|
Writing a correct each
A good each follows five rules, all of them visible in the real Catalog#each:
-
Yield every element, and return
selfwhen a block is given. Callers chain on the result, andArray#each,Hash#eachandRange#eachall return the receiver.each_sliceandeach_consreturn the receiver since Ruby 3.1 (they returnednilin Ruby 3.0). -
Return an
Enumeratorwhen no block is given, withreturn to_enum(:each) unless block_given?orenum_for. Without iteachraisesLocalJumpError("no block given (yield)") as soon as someone callsmapwithout a block,each_sliceorlazyon the object. An optional block afterto_enum(:each)computes the enumerator’ssize; without itsizeisnil. -
Forward the block with an anonymous
&(Ruby 3.1) when the elements come from another enumerable:@store.all.each(&)passes the caller’s block straight through, whatever its arity. -
Yield one value per element, or a deliberate pair.
yield a, bis not the same asyield [a, b]: see Yielding several values. -
Do not mutate the collection while iterating and, if other threads may add elements, iterate over a snapshot as
Catalogdoes (the lock is held only while the snapshot is taken, never while the caller’s block runs).
Methods that need every element (sort, sort_by, min, max, sum, cycle, reverse_each, to_a) call each to
completion, and sort, cycle and reverse_each also buffer the result, so an each that never ends makes them hang. Methods that stop early (first, take, take_while,
find, include?, any?, all?, none?, each_slice(n).first, lazy.first) are safe on an endless each.
Yielding several values
If each yields two values, Enumerable methods that collect elements pack them into an Array, while methods that pass the
element to your block split or take the first value depending on the block’s parameters. each_entry is the variant that
always passes exactly one object to its block: several yielded values arrive as one Array. Hash#each yields one
two-element Array, so a Hash behaves like the packed case.
# frozen_string_literal: true
module Shelf
# Shelf sections with a book count, yielding two values per step.
class Sections
include Enumerable
def initialize(**counts)
@counts = counts
end
# Yields (name, count). Returns self with a block, an Enumerator without.
def each
return to_enum(:each) { @counts.size } unless block_given?
# Two values per step, on purpose: not one packed pair.
@counts.each { |name, count| yield name, count } # rubocop:disable Style/ExplicitBlockArgument
self
end
end
end
sections = Shelf::Sections.new(fiction: 12, science: 7, history: 3)
sections.to_a # => [[:fiction, 12], [:science, 7], [:history, 3]]
sections.map { |name, count| "#{name}=#{count}" } # => ["fiction=12", "science=7", "history=3"]
sections.map { |pair| pair } # => [:fiction, :science, :history]
sections.each_entry.to_a # => [[:fiction, 12], [:science, 7], [:history, 3]]
sections.first # => [:fiction, 12]
sections.min_by { |_name, count| count } # => [:history, 3]
sections.sort_by { |_name, count| -count }.first(2) # => [[:fiction, 12], [:science, 7]]
sections.each.size # => 3
sections.each { nil }.equal?(sections) # => true
sections.each_with_index.map { |(name, _), i| "#{i}:#{name}" } # => ["0:fiction", "1:science", "2:history"]
sections.to_h # => {fiction: 12, science: 7, history: 3}
sections.sum { |_name, count| count } # => 22
sections.each_slice(2).to_a # => [[[:fiction, 12], [:science, 7]], [[:history, 3]]]
sections.include?([:fiction, 12]) # => true
The pair-per-step results show three rules at once. to_a, first, min_by, to_h and each_slice see the packed pair;
map { |name, count| } destructures it, and map { |pair| } receives only the first yielded value. Do not rely on the last
behaviour: when an each yields several values, document it, and prefer yield [a, b] so that the single-parameter form
also receives both.
What goes wrong without a proper each
# frozen_string_literal: true
module Demo
# Enumerable calls each on the including object; without one, nothing works.
class Hollow
include Enumerable
end
# An each that always yields fails when it is called without a block.
class Eager
include Enumerable
def each
yield 1
yield 2
end
end
# An endless each is fine as long as the caller stops early.
class Counter
include Enumerable
def each
return to_enum(:each) unless block_given?
n = 0
loop { yield(n += 1) }
end
end
end
begin
Demo::Hollow.new.map { |x| x }
rescue NoMethodError => e
e.message # => "undefined method 'each' for an instance of Demo::Hollow"
end
Demo::Eager.new.map { |x| x * 2 } # => [2, 4]
begin
Demo::Eager.new.each
rescue LocalJumpError => e
e.message # => "no block given (yield)"
end
Demo::Counter.new.first(3) # => [1, 2, 3]
Demo::Counter.new.lazy.select(&:even?).first(2) # => [2, 4]
Demo::Counter.new.each_slice(2).first # => [1, 2]
Demo::Counter.new.take_while { |n| n < 4 } # => [1, 2, 3]
Demo::Counter.new.each.next # => 1
The three failures are the three rules above. A class that includes Enumerable without each raises NoMethodError the
first time any module method runs, an each that yields without checking block_given? raises LocalJumpError when called
bare, and an endless each is fine for first, take_while, lazy and each_slice(2).first, which stop pulling elements
as soon as they have enough.
Transforming: map, flat_map, filter_map, zip, to_h
map (alias collect) returns an Array of the block’s results, always the same size as the input. flat_map (alias
collect_concat) concatenates the block’s Array results one level deep, so it is map followed by flatten(1), and
filter_map (Ruby 2.7) is map that drops falsy results, replacing the idiom map { }.compact or select { }.map { }.
each_with_index, each_with_object and with_index supply positions and a memo object, zip walks several
enumerables in step (padding with nil when the arguments are shorter and returning nil when given a block), and to_h
with a block (Ruby 2.6) builds a Hash from [key, value] pairs. compact (Ruby 3.1) removes nil, uniq (Ruby 2.4)
removes duplicates by eql? and hash, or by the value the block returns.
# frozen_string_literal: true
titles = ["Ruby Basics", "Iterators", "Blocks and Procs"]
titles.map(&:length) # => [11, 9, 16]
titles.collect { |t| t.split.first } # => ["Ruby", "Iterators", "Blocks"]
titles.flat_map(&:split) # => ["Ruby", "Basics", "Iterators", "Blocks", "and", "Procs"]
titles.map(&:split) # => [["Ruby", "Basics"], ["Iterators"], ["Blocks", "and", "Procs"]]
titles.flat_map { |t| [t.split] } # => [["Ruby", "Basics"], ["Iterators"], ["Blocks", "and", "Procs"]]
titles.filter_map { |t| t.split.first if t.size > 12 } # => ["Blocks"]
titles.each_with_index.map { |t, i| "#{i}: #{t}" } # => ["0: Ruby Basics", "1: Iterators", "2: Blocks and Procs"]
titles.each.with_index(1).map { |t, i| "#{i}. #{t[0, 4]}" } # => ["1. Ruby", "2. Iter", "3. Bloc"]
titles.each_with_object(Hash.new(0)) { |t, memo| memo[t.size.odd?] += 1 } # => {true => 2, false => 1}
titles.zip([1, 2, 3], %w[a b c]) # => [["Ruby Basics", 1, "a"], ["Iterators", 2, "b"], ["Blocks and Procs", 3, "c"]]
titles.zip([1]) # => [["Ruby Basics", 1], ["Iterators", nil], ["Blocks and Procs", nil]]
titles.zip([1, 2, 3]) { |_pair| nil } # => nil
titles.to_h { |t| [t.split.first, t.size] } # => {"Ruby" => 11, "Iterators" => 9, "Blocks" => 16}
titles.each_entry.to_a # => ["Ruby Basics", "Iterators", "Blocks and Procs"]
[3, 1, 3, nil, 2, nil, 1].compact # => [3, 1, 3, 2, 1]
[3, 1, 3, 2, 1].uniq # => [3, 1, 2]
titles.uniq { |t| t.size.odd? } # => ["Ruby Basics", "Blocks and Procs"]
titles.to_set.size # => 3
titles.entries # => ["Ruby Basics", "Iterators", "Blocks and Procs"]
titles.cycle.first(4) # => ["Ruby Basics", "Iterators", "Blocks and Procs", "Ruby Basics"]
titles.chain(%w[extra]).to_a # => ["Ruby Basics", "Iterators", "Blocks and Procs", "extra"]
(titles.each + %w[extra]).class # => Enumerator::Chain
Two details deserve attention. flat_map flattens only one level: a block that returns [t.split] leaves the inner Array in
place, and titles.map(&:split) and titles.flat_map { |t| [t.split] } are identical. And each.with_index(1) starts
counting at one, which is clearer than adding one inside the block. Enumerable#each_entry and entries are rarely needed
in application code; they exist so that code can turn any enumerable, whatever it yields, into an Array of its elements.
Searching and filtering
select (aliases filter, since Ruby 2.6, and find_all) keeps the elements for which the block is truthy; reject keeps
the others; partition returns both groups in one pass. find (alias detect) returns the first match and stops, and takes
an optional callable that is invoked, and its result returned, when nothing matches. find_index returns the position of the
first match (or of the first element == to the argument), first, take and drop cut by position, and take_while and
drop_while cut by a condition that is checked until it first fails. count counts everything, equal elements or matching
elements. include? (alias member?) tests equality with ==.
grep and grep_v select with the case-equality operator ===, so they accept classes, ranges and regular expressions,
and grep takes a block that transforms what it matched. Since Ruby 3.0 a block-less grep no longer sets $~, but with a
block Regexp.last_match is still available inside the block.
# frozen_string_literal: true
titles = ["Ruby Basics", "Iterators", "Blocks and Procs", "ruby tips", "Pattern Matching"]
titles.select { |t| t.size > 10 } # => ["Ruby Basics", "Blocks and Procs", "Pattern Matching"]
titles.filter { |t| t.size > 10 } # => ["Ruby Basics", "Blocks and Procs", "Pattern Matching"]
titles.find_all { |t| t.size > 10 } # => ["Ruby Basics", "Blocks and Procs", "Pattern Matching"]
titles.reject { |t| t.downcase.start_with?("r") } # => ["Iterators", "Blocks and Procs", "Pattern Matching"]
titles.find { |t| t.start_with?("B") } # => "Blocks and Procs"
titles.detect { |t| t.start_with?("Z") } # => nil
titles.find(-> { "none" }) { |t| t.start_with?("Z") } # => "none"
titles.find_index { |t| t.include?("Procs") } # => 2
titles.find_index("Iterators") # => 1
titles.first # => "Ruby Basics"
titles.first(2) # => ["Ruby Basics", "Iterators"]
titles.take(2) # => ["Ruby Basics", "Iterators"]
titles.drop(3) # => ["ruby tips", "Pattern Matching"]
titles.take_while { |t| t.size > 9 } # => ["Ruby Basics"]
titles.drop_while { |t| t.size > 9 } # => ["Iterators", "Blocks and Procs", "ruby tips", "Pattern Matching"]
titles.partition { |t| t.match?(/\A[A-Z]/) }.map(&:size) # => [4, 1]
titles.grep(/ruby/i) # => ["Ruby Basics", "ruby tips"]
titles.grep(/(\w+) tips/) { Regexp.last_match(1) } # => ["ruby"]
titles.grep_v(/ruby/i) # => ["Iterators", "Blocks and Procs", "Pattern Matching"]
titles.grep(10..12) # => []
titles.map(&:size).grep(10..12) # => [11]
titles.map(&:size).grep_v(Integer) # => []
titles.count # => 5
titles.count("Iterators") # => 1
titles.count { |t| t.size.even? } # => 2
titles.include?("ruby tips") # => true
titles.member?("nothing") # => false
titles.grep(10..12) returned [] because (10..12) === "Ruby Basics" is false: grep uses === of the argument on each
element, so a range matches numbers, not their string lengths. The last two lines show the correct form on the sizes.
Array#find is a native method since Ruby 4.0 (it is faster than the inherited Enumerable#find and has the same
semantics), and Ruby 4.0 also adds Array#rfind, which replaces reverse_each.find. On any other enumerable find is
still the module’s. The same holds for first, take, min, sum, uniq and the other methods listed as overridden in
the earlier table.
Predicates: all?, any?, none?, one?
The four predicates take a block, a pattern (Ruby 2.5) or nothing. With a pattern, an element matches when
pattern === element; with nothing, an element matches when it is truthy. They short-circuit: any? stops at the first
match, all? at the first failure, none? and one? as soon as the answer is known. On an empty collection all? and
none? are true (vacuous truth) and any? and one? are false.
# frozen_string_literal: true
loans = [
{ title: "Ruby Basics", days: 3, state: :lent },
{ title: "Iterators", days: 20, state: :overdue },
{ title: "Blocks and Procs", days: 1, state: :returned }
]
loans.all? { |l| l[:days].positive? } # => true
loans.any? { |l| l[:state] == :overdue } # => true
loans.none? { |l| l[:days] > 30 } # => true
loans.one? { |l| l[:state] == :lent } # => true
loans.map { |l| l[:days] }.all?(1..30) # => true
loans.map { |l| l[:title] }.any?(/Procs/) # => true
loans.map { |l| l[:title] }.none?(Integer) # => true
loans.map { |l| l[:state] }.one?(:overdue) # => true
[nil, false, 0].any? # => true
[nil, false].none? # => true
[1, nil].all? # => false
[0, nil].one? # => true
[].all? # => true
[].any? # => false
[].none? # => true
[].one? # => false
(1..).any? { |n| n > 100 } # => true
(1..).lazy.map { |n| n * 2 }.include?(10) # => true
The patterns make the code read like the intention: states.one?(:overdue), days.all?(1..30) and titles.any?(/Procs/)
replace explicit blocks. The last two lines work on infinite sequences because the predicates stop early; include? on a
lazy chain does the same.
Sorting and extremes
sort compares elements with <=> (or with a block that returns a negative number, zero or a positive number), sort_by
computes a key for each element once and sorts by the keys, and min, max and minmax return the extremes. All three
accept a count (max(2)) that returns an Array, and a block that replaces <=>. min_by, max_by and minmax_by
extract a key per element like sort_by. sort_by evaluates the block once per element and compares the keys, so it is
the right choice when the key is expensive; sort { |a, b| ... } calls the block for every comparison.
The documentation states that the ordering of equal elements is indeterminate for sort, sort_by, min, max and
their variants, so none of them is guaranteed to be stable. When ties must keep their original order, add the position as a
tie-breaker: sort_by.with_index { |x, i| [key(x), i] }. Sorting by several criteria is done by returning an Array as the
key, and descending order by negating a numeric key (-t.size) or by reversing the comparison.
Comparing values that <=> cannot compare (it returns nil) raises ArgumentError with the message
comparison of A with B failed. This is the most common error in this family: a nil among the keys, or a mix of Integers
and Strings.
# frozen_string_literal: true
titles = ["Ruby Basics", "Blocks and Procs", "Iterators in Depth", "ruby tips"]
titles.sort # => ["Blocks and Procs", "Iterators in Depth", "Ruby Basics", "ruby tips"]
titles.sort { |a, b| a.casecmp(b) } # => ["Blocks and Procs", "Iterators in Depth", "Ruby Basics", "ruby tips"]
titles.sort_by(&:downcase) # => ["Blocks and Procs", "Iterators in Depth", "Ruby Basics", "ruby tips"]
titles.sort_by { |t| [-t.size, t] } # => ["Iterators in Depth", "Blocks and Procs", "Ruby Basics", "ruby tips"]
titles.min # => "Blocks and Procs"
titles.max(2) # => ["ruby tips", "Ruby Basics"]
titles.min(2) { |a, b| a.size <=> b.size } # => ["ruby tips", "Ruby Basics"]
titles.minmax # => ["Blocks and Procs", "ruby tips"]
titles.min_by(&:size) # => "ruby tips"
titles.max_by(2, &:size) # => ["Iterators in Depth", "Blocks and Procs"]
titles.minmax_by(&:size) # => ["ruby tips", "Iterators in Depth"]
[].min # => nil
[].max(2) # => []
[].sum # => 0
[].inject(:+) # => nil
begin
titles.sort_by { |t| t.size.odd? ? t.size : nil }
rescue ArgumentError => e
e.message # => "comparison of Integer with nil failed"
end
begin
[3, "a"].sort
rescue ArgumentError => e
e.message # => "comparison of Integer with String failed"
end
begin
[Object.new, Object.new].max
rescue ArgumentError => e
e.message # => "comparison of Object with Object failed"
end
titles.sort puts "ruby tips" last because uppercase letters sort before lowercase ones; sort_by(&:downcase) or
sort { |a, b| a.casecmp(b) } give a case-insensitive order. min and max of an empty collection return nil, max(2)
returns [], sum returns 0 and inject(:+) returns nil: always decide what the empty case should produce.
Comparable is what makes custom objects sortable. A class that defines <=> and includes Comparable gets <, <=,
==, >, >=, between? and clamp, and every Enumerable method that ranks elements then works on collections of it
(an example is in The Iterator pattern and Comparable).
Reducing: inject, reduce and sum
inject (alias reduce) folds the elements into one value. It has four forms: a block (inject { |acc, x| ... }, where
the first element is the initial accumulator), an initial value and a block, a method name as a Symbol (inject(:+)), and an initial value and a Symbol (inject(10, :+)). The block’s return value becomes the next
accumulator, which is the cause of the most common bug with it. On an empty collection the block forms return the initial
value or nil.
sum is a specialised inject(:+) that starts at 0 (or at the argument, which is required for anything that is not a
number: %w[a b].sum("")), accepts a block, and, for Floats, uses Kahan-Babuska compensated summation, so it is more precise
than a naive loop. Integer ranges are summed arithmetically without visiting the elements. Array#sum has existed since
Ruby 2.4 and does not call each; Enumerable#sum calls it.
tally (Ruby 2.7) counts occurrences and returns a Hash; since Ruby 3.1 it accepts a Hash to add to. There is no
tally_by in core Ruby (the example below shows the NoMethodError): count by a derived key with group_by then transform_values(&:size),
or map then tally.
# frozen_string_literal: true
days = [3, 20, 1, 7]
days.inject(:+) # => 31
days.inject(100, :+) # => 131
days.inject { |sum, n| sum + n } # => 31
days.reduce(1) { |product, n| product * n } # => 420
days.inject(:lcm) # => 420
days.sum # => 31
days.sum(0.0) # => 31.0
days.sum { |n| n * 2 } # => 62
%w[a b c].sum("") # => "abc"
[[1], [2, 3]].sum([]) # => [1, 2, 3]
[0.1, 0.2, 0.3].sum # => 0.6
[0.1, 0.2, 0.3].inject(:+) # => 0.6000000000000001
(1..4).each_entry.sum # => 10
%w[a b a c a b].tally # => {"a" => 3, "b" => 2, "c" => 1}
%w[a b a].tally(Hash.new(0).merge("z" => 5)) # => {"z" => 5, "a" => 2, "b" => 1}
begin
%w[a b c].sum
rescue TypeError => e
e.message # => "String can't be coerced into Integer"
end
begin
%w[apple avocado bean].tally_by { |w| w[0] }
rescue NoMethodError => e
e.message # => "undefined method 'tally_by' for an instance of Array"
end
begin
days.inject(:no_such)
rescue NoMethodError => e
e.message # => "undefined method 'no_such' for an instance of Integer"
end
[1, 2, 3].each_with_object([]) { |n, acc| acc << (n * 2) } # => [2, 4, 6]
[1, 2, 3].inject([]) { |acc, n| acc << (n * 2) } # => [2, 4, 6]
[1, 2, 3].inject({}) { |acc, n| acc.merge(n => (n * n)) } # => {1 => 1, 2 => 4, 3 => 9}
[0.1, 0.2, 0.3].sum is 0.6 while inject(:+) gives 0.6000000000000001: use sum for Floats. The last three lines are
the accumulator rule. each_with_object mutates one object that it passes in and returns it, so the block’s value is
irrelevant; inject threads the block’s return value through, so a block that ends with an assignment
(acc[n] = true) returns true and breaks the next iteration, and the version that merges returns a new Hash each time.
merge! or acc[n] = true; acc also work, but for building a Hash each_with_object, to_h { } or group_by is
clearer.
Grouping, chunking and slicing
These methods restructure a sequence. group_by returns a Hash from the block’s value to the Array of elements that
produced it. partition splits into two Arrays. each_slice(n) produces non-overlapping groups of at most n elements
(the last may be shorter), and each_cons(n) produces every run of n consecutive elements, overlapping. Both return the
receiver when given a block (Ruby 3.1) and an Enumerator without one, and both raise ArgumentError ("invalid slice size")
for 0.
The sequence-splitting methods return Enumerators, so call to_a, map or first on them:
-
chunk { |x| key }groups consecutive elements with the same key. A key ofnilor:_separatordrops the element and ends the chunk; a key of:_alonemakes the element a chunk of its own; any other Symbol that starts with an underscore raisesRuntimeError("symbols beginning with an underscore are reserved"). -
chunk_while { |a, b| ... }keeps two neighbours in the same chunk while the block is true for them, andslice_whenstarts a new chunk when the block is true. They are complements:chunk_while { |a, b| b == a + 1 }andslice_when { |a, b| b != a + 1 }give the same runs. -
slice_before(pattern or block)starts a new slice before a matching element, andslice_afterends the slice after one.
# frozen_string_literal: true
days = [1, 2, 4, 5, 6, 9, 10]
titles = %w[Ruby Blocks Procs Pattern Matches]
titles.group_by(&:size) # => {4 => ["Ruby"], 6 => ["Blocks"], 5 => ["Procs"], 7 => ["Pattern", "Matches"]}
titles.group_by { |t| t[0].downcase }.transform_values(&:size) # => {"r" => 1, "b" => 1, "p" => 2, "m" => 1}
days.partition(&:even?) # => [[2, 4, 6, 10], [1, 5, 9]]
days.chunk_while { |a, b| b == a + 1 }.to_a # => [[1, 2], [4, 5, 6], [9, 10]]
days.slice_when { |a, b| b > a + 1 }.map(&:minmax) # => [[1, 2], [4, 6], [9, 10]]
days.chunk(&:even?).to_a # => [[false, [1]], [true, [2, 4]], [false, [5]], [true, [6]], [false, [9]], [true, [10]]]
days.chunk { |n| n > 5 ? :big : nil }.to_a # => [[:big, [6, 9, 10]]]
days.chunk { |n| n == 5 ? :_separator : true }.to_a # => [[true, [1, 2, 4]], [true, [6, 9, 10]]]
days.chunk { |n| n.even? ? :_alone : :odd }.first(3) # => [[:odd, [1]], [:_alone, [2]], [:_alone, [4]]]
titles.slice_before { |t| t.start_with?("P") }.to_a # => [["Ruby", "Blocks"], ["Procs"], ["Pattern", "Matches"]]
titles.slice_after { |t| t.end_with?("s") }.to_a # => [["Ruby", "Blocks"], ["Procs"], ["Pattern", "Matches"]]
titles.slice_before(/\AP/).to_a # => [["Ruby", "Blocks"], ["Procs"], ["Pattern", "Matches"]]
days.each_slice(3).to_a # => [[1, 2, 4], [5, 6, 9], [10]]
days.each_cons(3).first(2) # => [[1, 2, 4], [2, 4, 5]]
days.each_cons(2).map { |a, b| b - a } # => [1, 2, 1, 1, 3, 1]
days.each_slice(3) { |slice| slice }.equal?(days) # => true
days.reverse_each.first(2) # => [10, 9]
days.each_with_index.select { |n, i| n.even? && i.odd? } # => [[2, 1]]
days.each_with_index.to_a.last # => [10, 6]
days.zip(days.drop(1)).first(2) # => [[1, 2], [2, 4]]
days.cycle(2).count # => 14
days.min(2) # => [1, 2]
days.lazy.select(&:even?).map { |n| n * 3 }.first(2) # => [6, 12]
days.each_cons(2).map { |a, b| b - a } is the idiomatic way to compute differences between neighbours, and
each_slice(3) is the idiomatic way to batch work (a database import, an API with a page size). cycle repeats the
sequence forever, or n times, and buffers the first pass in memory. reverse_each buffers the whole sequence too.
min(2) and max(2) state the intent directly, and RuboCop reports sort.first and sort_by { }.last in favour of min and max_by (Style/RedundantSort).
Methods without a block return enumerators
Nearly every Enumerable method that takes a block returns an Enumerator when called without one. The enumerator
remembers the method and the receiver, so map.with_index(1) { |x, i| ... } is "map, passing the index too", and
select.with_index { |x, i| i.even? } is "select by position". The enumerator’s size is computed without iterating
where the method can know it, and is nil otherwise (for example for select, chunk and slice_when). lazy returns an
Enumerator::Lazy, which defers each stage until the result is requested.
# frozen_string_literal: true
titles = %w[Ruby Iterators Blocks]
titles.map.class # => Enumerator
titles.select.class # => Enumerator
titles.each_slice(2).class # => Enumerator
titles.group_by.class # => Enumerator
titles.map.with_index(1) { |t, i| "#{i}:#{t}" } # => ["1:Ruby", "2:Iterators", "3:Blocks"]
titles.select.with_index { |_t, i| i.even? } # => ["Ruby", "Blocks"]
titles.each_with_index.to_a # => [["Ruby", 0], ["Iterators", 1], ["Blocks", 2]]
titles.each.with_object([]).class # => Enumerator
titles.each_slice(2).size # => 2
titles.each_cons(2).size # => 2
titles.map.size # => 3
titles.cycle.size # => Infinity
[].cycle.size # => 0
titles.lazy.class # => Enumerator::Lazy
titles.lazy.map(&:size).class # => Enumerator::Lazy
titles.lazy.map(&:size).to_a # => [4, 9, 6]
(1..Float::INFINITY).lazy.select(&:even?).map { |n| n * n }.first(3) # => [4, 16, 36]
(1..).each_slice(2).first(2) # => [[1, 2], [3, 4]]
titles.each_entry.class # => Enumerator
titles.chain(1..2).class # => Enumerator::Chain
titles.chain(1..2).size # => 5
titles.cycle.size is Infinity and [].cycle.size is 0. The chain method (Ruby 2.6) and Enumerator#+ produce an
Enumerator::Chain that iterates the receiver and then each argument, with a size that is the sum of the parts. The
semantics of lazy enumerators, external iteration and Enumerator.new are in
Enumerators and Lazy Evaluation.
Hash, Range, Struct and Set as enumerables
Each class decides what each yields, and the rest follows:
| Class | each yields |
Notes |
|---|---|---|
|
each element |
overrides 31 of the 61 methods; |
|
a two-element Array |
overrides |
|
each element, via |
overrides |
|
each member value |
|
|
not Enumerable |
use |
|
each element |
overrides only |
|
a line (IO) or an entry (Dir) |
|
|
a |
|
# frozen_string_literal: true
stock = { "Ruby Basics" => 3, "Iterators" => 0, "Blocks and Procs" => 5 }
stock.map { |title, n| "#{title}: #{n}" } # => ["Ruby Basics: 3", "Iterators: 0", "Blocks and Procs: 5"]
stock.map { |pair| pair }.first # => ["Ruby Basics", 3]
stock.select { |_title, n| n.positive? } # => {"Ruby Basics" => 3, "Blocks and Procs" => 5}
stock.reject { |_title, n| n > 4 } # => {"Ruby Basics" => 3, "Iterators" => 0}
stock.filter_map { |title, n| title if n.positive? } # => ["Ruby Basics", "Blocks and Procs"]
stock.find { |_title, n| n.zero? } # => ["Iterators", 0]
stock.min_by { |_title, n| n } # => ["Iterators", 0]
stock.sort_by { |_title, n| -n }.first(2) # => [["Blocks and Procs", 5], ["Ruby Basics", 3]]
stock.sum { |_title, n| n } # => 8
stock.count { |_title, n| n.positive? } # => 2
stock.partition { |_title, n| n.zero? }.map(&:size) # => [1, 2]
stock.group_by { |_title, n| n.zero? }.keys # => [false, true]
stock.each_with_index.map { |(title, _n), i| "#{i}:#{title[0]}" } # => ["0:R", "1:I", "2:B"]
stock.each_with_object([]) { |(title, n), acc| acc << title if n > 2 } # => ["Ruby Basics", "Blocks and Procs"]
stock.to_a.transpose # => [["Ruby Basics", "Iterators", "Blocks and Procs"], [3, 0, 5]]
stock.zip([1, 2, 3]).first # => [["Ruby Basics", 3], 1]
stock.each_slice(2).map(&:to_h) # => [{"Ruby Basics" => 3, "Iterators" => 0}, {"Blocks and Procs" => 5}]
stock.inject(0) { |sum, (_title, n)| sum + n } # => 8
stock.any? { |_title, n| n.zero? } # => true
stock.all?(Array) # => true
Point = Struct.new(:x, :y)
Point.new(1, 2).map { |v| v * 10 } # => [10, 20]
Point.include?(Enumerable) # => true
Data.define(:x).include?(Enumerable) # => false
(1..3).each_cons(2).to_a # => [[1, 2], [2, 3]]
("a".."e").each_slice(2).map(&:join) # => ["ab", "cd", "e"]
(1..10).step(3).map { |n| n } # => [1, 4, 7, 10]
1.step(10, 4).to_a # => [1, 5, 9]
Set[3, 1, 2].sort # => [1, 2, 3]
Set[3, 1, 2].map { |n| n * 2 } # => [6, 2, 4]
Set[3, 1, 2].select(&:odd?).class # => Array
ENV.singleton_class.include?(Enumerable) # => true
Dir.include?(Enumerable) # => true
IO.include?(Enumerable) # => true
Enumerable.instance_method(:map).owner # => Enumerable
Array.instance_method(:map).owner # => Array
Hash.instance_method(:select).owner # => Hash
Hash.instance_method(:map).owner # => Enumerable
Range.instance_method(:count).owner # => Range
The pair rule explains what looks like inconsistency. stock.map { |pair| pair }.first is ["Ruby Basics", 3]: the block
received the packed pair. stock.select { } returns a Hash because Hash overrides select, while stock.partition { }
returns Arrays of pairs because partition is the module’s. To get a Hash back from a module method, finish with to_h.
Set#select returns an Array, which surprises people who expect a Set; use select! to mutate the set, or finish with
to_set when a Set is wanted.
Block arity and destructuring
A block (a proc, not a lambda) with several parameters destructures a single Array argument, so { |title, n| ... }
works on a Hash pair and { |a, (b, c)| ... } destructures a nested one. A block with one parameter receives the packed
Array. Lambdas and method objects are strict about arity. Ruby 3.0 made Hash#each consistently yield one two-element
Array, so a lambda with two parameters passed to each or min_by raises ArgumentError, while map(&lambda) still
accepts it, as the next file shows. Because the behaviour differs between methods, prefer a plain block when the element
is a pair.
# frozen_string_literal: true
stock = { "Ruby Basics" => 3, "Iterators" => 0 }
# A block with two parameters destructures the [key, value] pair.
stock.map { |title, n| "#{title}=#{n}" } # => ["Ruby Basics=3", "Iterators=0"]
stock.each_with_index.map { |(title, n), i| [i, title, n] } # => [[0, "Ruby Basics", 3], [1, "Iterators", 0]]
# A lambda is strict about arity, and its behaviour differs between methods.
pair_lambda = ->(title, n) { "#{title}=#{n}" }
stock.map(&pair_lambda) # => ["Ruby Basics=3", "Iterators=0"]
begin
stock.each(&pair_lambda)
rescue ArgumentError => e
e.message # => "wrong number of arguments (given 1, expected 2)"
end
begin
stock.min_by(&pair_lambda)
rescue ArgumentError => e
e.message # => "wrong number of arguments (given 1, expected 2)"
end
# Arity of the block decides how an array element is split.
[[1, 2], [3, 4]].map { |a, b| a + b } # => [3, 7]
[[1, [2, 3]]].map { |a, (b, c)| [a, b, c] } # => [[1, 2, 3]]
[[1, 2], [3, 4]].map { |a| a } # => [[1, 2], [3, 4]]
[[1, 2], [3, 4]].each_slice(1).map(&:first) # => [[1, 2], [3, 4]]
# Argument errors are reported with the usual messages.
begin
[1, 2, 3].inject(10)
rescue TypeError => e
e.message # => "10 is not a symbol nor a string"
end
begin
[1, 2].min(-1)
rescue ArgumentError => e
e.message # => "negative size (-1)"
end
begin
[1, 2].chunk { :_reserved }.to_a
rescue RuntimeError => e
e.message # => "symbols beginning with an underscore are reserved"
end
begin
[[1, 2], [3]].to_h
rescue ArgumentError => e
e.message # => "wrong array length at 1 (expected 2, was 1)"
end
Choosing iterators over loops
Ruby has while, until, for and loop, but application code is overwhelmingly written with iterators, for four reasons.
-
The iterator owns the bookkeeping. An index variable, an accumulator and an end condition are three places for an off-by-one error;
each_with_index,sumandeach_slicehave none of them. -
Scope. The variable of a
forloop is not local to it, so it leaks into the surrounding method, whereas a block parameter does not. Aforloop is implemented witheach, so it is not a faster alternative. -
A result.
map,selectandsumare expressions that produce the value, where a loop needs a variable that is initialised before it and mutated inside. -
A name for the intent.
select,reject,partitionandgroup_bysay what is being done; a loop with anifmakes the reader work it out.
Loops still have a place in algorithms that are not a traversal of a collection: a counter with a complex step, a loop that
changes its own bounds (binary search, a two-pointer scan), or a state machine. For fixed counts, prefer times,
upto, downto and step (on Integer, Float and ranges; Numeric#step and Range#step return an arithmetic sequence).
loop is the idiomatic endless loop; it ends when the block raises StopIteration and returns the exception’s result,
which is the enumerator’s result when you drive an external iterator with next.
# frozen_string_literal: true
titles = ["Ruby Basics", "Iterators", "Blocks and Procs"]
# C-style counting loop versus the iterator that owns the index.
numbered = []
i = 0
while i < titles.size
numbered << "#{i}: #{titles[i]}"
i += 1
end
numbered.last # => "2: Blocks and Procs"
titles.each_with_index.map { |t, idx| "#{idx}: #{t}" }.eql?(numbered) # => true
# `for` is `each` underneath, but its variable leaks into the enclosing scope.
for title in titles do end # rubocop:disable Style/For
title.upcase # => "BLOCKS AND PROCS"
binding.local_variable_defined?(:t) # => false
3.times.map { |n| n * n } # => [0, 1, 4]
1.upto(4).to_a # => [1, 2, 3, 4]
4.downto(2).to_a # => [4, 3, 2]
1.step(10, 3).to_a # => [1, 4, 7, 10]
(1..10).step(3).to_a # => [1, 4, 7, 10]
10.step(1, -4).to_a # => [10, 6, 2]
titles.map { it.split.first } # => ["Ruby", "Iterators", "Blocks"]
# rubocop:disable-next Style/ItBlockParameter
titles.map { _1.split.first } # => ["Ruby", "Iterators", "Blocks"]
titles.map(&:size) # => [11, 9, 16]
# loop rescues StopIteration and returns the enumerator's result.
enum = titles.each
seen = []
result = loop do
seen << enum.next.upcase
end
[seen.size, result.equal?(titles)] # => [3, true]
# break returns a value from the iterator; next skips to the next element.
titles.each { |t| break t if t.include?("Procs") } # => "Blocks and Procs"
labels = titles.map do |t|
next "(skipped)" if t.size < 10
t.upcase
end
labels.first(2) # => ["RUBY BASICS", "(skipped)"]
titles.each_with_index.find { |t, _i| t.size > 10 } # => ["Ruby Basics", 0]
# Extract a block body into a named method and pass it with &method.
def short?(title) = title.size < 12
titles.select(&method(:short?)) # => ["Ruby Basics", "Iterators"]
titles.grep_v(/Procs/).all?(&method(:short?)) # => true
# An accumulating loop and its Enumerable equivalents.
total = 0
titles.each { |t| total += t.size }
titles.sum(&:size).eql?(total) # => true
titles.inject(0) { |sum, t| sum + t.size } # => 36
Three details of the file are worth stating. After the for loop, title is still "Blocks and Procs", and t from the block
is not defined, so binding.local_variable_defined?(:t) is false. titles.map { it.split.first } uses the it parameter
introduced in Ruby 3.4, the preferred form for a one-parameter block; _1 (numbered parameters, Ruby 2.7) still works,
but RuboCop’s Style/ItBlockParameter asks for it, and _1 together with _2 is limited by
Style/NumberedParametersLimit. And break value leaves the whole iterator and makes it return value, while next value
only ends the current block call, the way return ends a method.
Extracting named methods from blocks
A block body longer than a line or two, or one that is reused, is easier to read and test as a method. &method(:name)
turns a method into a block, a lambda stored in a constant can be passed with &, and a refactoring such as
select(&method(:short?)) makes the call site read as a sentence. Name the predicate after the question it answers
(overdue?, short?), and keep the iteration (select, map) in the caller.
Pitfalls
# frozen_string_literal: true
# Deleting from an array while iterating over it skips elements.
queue = %w[a b c d]
seen = []
queue.each do |item|
seen << item
queue.delete(item)
end
[seen, queue] # => [["a", "c"], ["b", "d"]]
# Iterate over a copy, or build a new collection instead.
queue = %w[a b c d]
queue.dup.each { |item| queue.delete(item) if item < "c" }
queue.size # => 2
%w[a b c d].reject { |item| item < "c" } # => ["c", "d"]
# A hash may not gain keys while it is iterated.
stock = { "Ruby Basics" => 3 }
begin
stock.each { |title, n| stock["#{title} 2"] = n }
rescue RuntimeError => e
[e.class, e.message] # => [RuntimeError, "can't add a new key into hash during iteration"]
end
stock.each_key { |title| stock.delete(title) }
stock.size # => 0
# Bad sizes, bad arguments and an accumulator that is not returned.
begin
[1, 2, 3].first(-1)
rescue ArgumentError => e
e.message # => "negative array size"
end
begin
[1, 2, 3].each_slice(0)
rescue ArgumentError => e
e.message # => "invalid slice size"
end
begin
[1, 2, 3].zip(4)
rescue TypeError => e
e.message # => "wrong argument type Integer (must respond to :each)"
end
begin
[1, 2, 3].inject({}) { |acc, n| acc[n] = true } # rubocop:disable Lint/UnmodifiedReduceAccumulator
rescue NoMethodError => e
e.message # => "undefined method '[]=' for true"
end
# The accumulator must be returned from inject's block, not from each_with_object's.
[1, 2, 3].each_with_object(Hash.new(0)) { |n, acc| acc[n.odd?] += n } # => {true => 4, false => 2}
[1, 2, 3].inject(Hash.new(0)) { |acc, n| acc.merge(n.odd? => acc[n.odd?] + n) } # => {true => 4, false => 2}
# first stops early; to_a and map on an endless range never return.
(1..).first(3) # => [1, 2, 3]
(1..).lazy.map { |n| n * 2 }.first(3) # => [2, 4, 6]
(1..).each_slice(2).first # => [1, 2]
# break leaves the whole iteration; next only the current block call.
[1, 2, 3, 4].each { |n| break n * 10 if n > 2 } # => 30
[1, 2, 3, 4].map { |n| n > 2 ? break : n } # => nil
[1, 2, 3, 4].select { |n| next true if n.odd? } # => [1, 3]
Source: Ruby 4.0 documentation: Hash
-
Mutation during iteration. Removing from an Array while iterating skips the element after each removal; iterate over a copy (
dup) or build a new collection withreject/select. A Hash raisesRuntimeError("can’t add a new key into hash during iteration") if a key is added ineach; deleting or updating existing keys is allowed. -
The accumulator.
injectneeds the block to return the accumulator;each_with_objectdoes not. -
Endless sources.
first,take,find,lazyandeach_slice(n).firststop early;to_a,map,sort,sum,cyclewithout a count andreverse_eachon an endless range never return. -
break,nextandreturn.breakinsidemapends the whole call and returnsnil(or the value given tobreak);nextends the current block call only and supplies that element’s value;returninside a block returns from the enclosing method. -
firstversustake. Both return the leading elements;firstwithout an argument returns one element (ornil),take(n)always returns an Array and raisesArgumentError("attempt to take negative size") for a negativen. -
Empty collections.
min,max,min_by,injectwithout an initial value andfirstreturnnil;sumreturns0;all?andnone?returntrue. State what you want in the code or pass an initial value. -
Strings are not Enumerable.
String#each_char,each_lineandeach_bytereturn enumerators, but a String does not include the module, so"abc".mapis aNoMethodError; usechars.maporeach_char.map.
The Iterator pattern and Comparable
The classic Iterator pattern, in which a collection exposes a way to walk its elements without exposing its structure, is
include Enumerable plus each in Ruby. Internal iteration (each with a block) is the default; external iteration
(next, peek, rewind on the enumerator returned by each) covers the cases where the caller needs to control the pace,
such as merging two sequences. Comparable plus <=> on the element type is the matching pattern for ordering.
# frozen_string_literal: true
require "shelf"
def book(isbn, title, author) = Shelf::Book.new(Shelf::Isbn.new(isbn), title, Shelf::Author.new(author))
catalog = Shelf::Catalog.new
catalog.add(book("978-0-306-40615-7", "Ruby Basics", "A. Writer"))
.add(book("978-1-234-56789-7", "Iterators in Depth", "B. Coder"))
# Catalog#each iterates over a snapshot, so adding while iterating is safe.
catalog.each { |b| catalog.add(b) if catalog.count < 4 }
catalog.count # => 4
# External iteration: the Iterator pattern, built into the language.
cursor = catalog.each
cursor.next.title # => "Ruby Basics"
cursor.peek.title # => "Iterators in Depth"
cursor.next.title # => "Iterators in Depth"
cursor.rewind.next.title # => "Ruby Basics"
begin
loop { cursor.next }
cursor.next
rescue StopIteration => e
e.message # => "iteration reached an end"
end
# Enumerable gives the Iterator pattern lazy, chained, composable views.
catalog.lazy.map(&:title).select { |t| t.include?("Depth") }.first # => "Iterators in Depth"
catalog.each_slice(2).map { |pair| pair.map { |b| b.title[0, 4] } } # => [["Ruby", "Iter"], ["Ruby", "Iter"]]
catalog.each_with_index.to_h { |b, i| [i, b.isbn.to_s] }.values.first # => "9780306406157"
# Comparable on the element turns on sort, min, max and minmax.
Edition = Data.define(:title, :year) do
include Comparable
def <=>(other) = year <=> other.year
end
editions = [Edition.new("Ruby Basics", 2019), Edition.new("Iterators", 2021), Edition.new("Blocks", 2017)]
editions.min.title # => "Blocks"
editions.max.title # => "Iterators"
editions.sort.map(&:year) # => [2017, 2019, 2021]
editions.minmax.map(&:title) # => ["Blocks", "Iterators"]
editions.sort { |a, b| b <=> a }.map(&:year) # => [2021, 2019, 2017]
editions.first.between?(editions.last, editions[1]) # => true
The message of StopIteration is "iteration reached an end", and loop rescues it. The Catalog snapshot is what keeps
catalog.each { catalog.add(b) } from looping forever or raising: a naive each over a live Array would see the appended
elements. Edition is declared with Data.define ... do include Comparable ... end (the current form; subclassing
Data.define(...) with class X < Data.define creates an extra anonymous class in the ancestors).
Performance and laziness
An eager chain such as select { }.map { }.first(3) runs every stage over every element and builds an intermediate Array for
each stage. Three techniques reduce that work, in this order of preference:
-
Use the most specific method.
findinstead ofselect.first,any?instead ofselect.any?,count { }instead ofselect { }.size,filter_mapinstead ofselect { }.map { },min_byinstead ofsort_by { }.first,sum { }instead ofmap { }.sum,each_sliceinstead of manual slicing. -
Stop early.
first,take_while,findandany?do not evaluate the rest of the collection. -
Go lazy with
lazywhen the source is very large, endless, or each stage is expensive and only the first few results matter; thenfirst(n)ortake(n).to_apulls only what is needed.
# frozen_string_literal: true
numbers = (1..1000).to_a
calls = Hash.new(0)
# An eager chain runs every stage over every element...
numbers.select { |n| calls[:select] += 1 and n.even? }
.map { |n| calls[:map] += 1 and n * 2 }
.first(3)
calls.dup # => {select: 1000, map: 500}
# ...a lazy chain pulls only as many elements as the result needs.
calls.clear
numbers.lazy
.select { |n| calls[:select] += 1 and n.even? }
.map { |n| calls[:map] += 1 and n * 2 }
.first(3)
calls.dup # => {select: 6, map: 3}
# find and first stop at the first hit; select.first does not.
calls.clear
numbers.find { |n| calls[:find] += 1 and n > 2 } # => 3
numbers.select { |n| calls[:select] += 1 and n > 2 }.first # => 3
calls.dup # => {find: 3, select: 1000}
# An integer Range is summed arithmetically, without visiting its elements.
(1..(10**18)).sum # => 500000000000000000500000000000000000
(1..(10**18)).each_entry.first(2).sum # => 3
The first block shows 1000 evaluations of select and 500 of map for the eager chain, and six and three for the lazy
one, which pulls elements one at a time through all the stages until first(3) has its three results. Lazy evaluation has
its own overhead, so on a small Array of cheap operations it can be slower than the eager chain; measure before changing, with the tools in
Performance, Benchmarking and Profiling.
RuboCop cops for iteration
These cops, from the default RuboCop configuration plus the pending cops enabled by NewCops: enable, enforce the habits
described above. Some are marked unsafe by RuboCop because it cannot prove the receiver’s type.
| Cop | What it reports |
|---|---|
|
a |
|
a block that only calls a method on its parameter (use |
|
a one-parameter block using |
|
more than one numbered parameter in a block |
|
|
|
consecutive |
|
|
|
|
|
|
|
a method that only yields what a block would (use |
|
|
|
|
|
custom logic that rejects |
|
an |
|
an expression whose value is dropped, such as a lone comparison |
Version notes
Each row was checked against the NEWS file of the version named, on docs.ruby-lang.org.
| Version | Change to Enumerable or its callers |
|---|---|
2.4 |
|
2.5 |
|
2.6 |
|
2.7 |
|
3.0 |
a block-less |
3.1 |
|
3.2 |
|
3.4 |
the |
4.0 |
|
Ruby 4.0 warns about the to_set change: Enumerable#to_set(Set) prints
warning: passing arguments to Enumerable#to_set is deprecated when called with a class argument. Call to_set with no
arguments and Set.new(enum, &block) if you need the block form.
Where older material is outdated
Tutorials and books written for Ruby 2.x and early 3.x teach correct ideas about iteration but show some details that have since changed. When reading older code or text on this topic, note these differences:
| Older material says | Current Ruby 4.0 |
|---|---|
numbered parameters |
use |
|
|
hash output such as |
|
|
|
|
they return the receiver since Ruby 3.1 |
|
|
a |
it is |
|
|
When a book builds an Iterator class with has_next? and next_item methods, replace it with each plus
include Enumerable and, if external stepping is needed, with enum_for and Enumerator#next.
Design guidance
-
Prefer
Enumerablemethods over index-based loops, and the most specific method over a chain of general ones. -
Give a collection class
eachfirst, theninclude Enumerable. Add<=>to the elements if they must sort. -
Return an Enumerator from
eachwhen no block is given, with a size when it is cheap to compute. -
Keep blocks to one or two lines and extract a named method when a block grows, is reused or needs a test.
-
Treat the empty case as part of the contract: pass an initial value to
injectandsumwhen the element type is not Integer. -
Use
lazyfor large or endless sources,each_slicefor batching andeach_consfor neighbours. -
Do not mutate a collection while iterating over it; iterate a copy or build a new collection.
-
Remember which methods return what:
Hash#selectreturns a Hash,Hash#mapandSet#mapreturn an Array.