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

all?, any?, none?, one?, include?/member?, count, tally

Booleans, an Integer or a Hash

Fetching

first, take, drop, take_while, drop_while, min, max, minmax, min_by, max_by, minmax_by, to_a/entries

Elements or Arrays

Searching and filtering

find/detect, find_all/select/filter, find_index, reject, uniq, grep, grep_v

An element, an index or an Array

Transforming

map/collect, flat_map/collect_concat, filter_map, zip, to_h, to_set, compact

A new Array, Hash or Set

Reducing

inject/reduce, sum

A single value

Sorting

sort, sort_by

A new Array

Grouping and slicing

group_by, partition, chunk, chunk_while, slice_when, slice_before, slice_after, each_slice, each_cons

A Hash, Arrays or an Enumerator

Iterating

each_entry, each_with_index, each_with_object, reverse_each, cycle

The receiver, the memo object or nil

Lazy and chained

lazy, chain

Enumerator::Lazy or Enumerator::Chain

flowchart TD Q["What do you need from the collection?"] --> A{"A single value?"} A -->|"yes"| R["inject, sum, min_by, max_by, count, tally, find, first"] A -->|"no"| B{"One result per element?"} B -->|"yes"| M["map, each_with_index, zip, sort_by"] B -->|"no, fewer"| F["select, reject, filter_map, take_while, grep, uniq"] B -->|"no, regrouped"| G["group_by, partition, chunk_while, slice_when, each_slice, each_cons, flat_map"]

Writing a correct each

A good each follows five rules, all of them visible in the real Catalog#each:

  1. Yield every element, and return self when a block is given. Callers chain on the result, and Array#each, Hash#each and Range#each all return the receiver. each_slice and each_cons return the receiver since Ruby 3.1 (they returned nil in Ruby 3.0).

  2. Return an Enumerator when no block is given, with return to_enum(:each) unless block_given? or enum_for. Without it each raises LocalJumpError ("no block given (yield)") as soon as someone calls map without a block, each_slice or lazy on the object. An optional block after to_enum(:each) computes the enumerator’s size; without it size is nil.

  3. 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.

  4. Yield one value per element, or a deliberate pair. yield a, b is not the same as yield [a, b]: see Yielding several values.

  5. Do not mutate the collection while iterating and, if other threads may add elements, iterate over a snapshot as Catalog does (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 of nil or :_separator drops the element and ends the chunk; a key of :_alone makes the element a chunk of its own; any other Symbol that starts with an underscore raises RuntimeError ("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, and slice_when starts a new chunk when the block is true. They are complements: chunk_while { |a, b| b == a + 1 } and slice_when { |a, b| b != a + 1 } give the same runs.

  • slice_before(pattern or block) starts a new slice before a matching element, and slice_after ends 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

Array

each element

overrides 31 of the 61 methods; group_by, partition, inject, flat_map, each_slice, each_with_index, tally, min_by, sort_by and lazy come from the module

Hash

a two-element Array [key, value]

overrides select/filter, reject, compact, to_h, any?, include?/member? and to_a; map, flat_map, filter_map, sort_by, min_by, sum, find, count and partition come from the module and return Arrays or pairs

Range

each element, via succ

overrides count, first, min, max, minmax, include?, reverse_each, to_a and to_set; endless ranges ((1..)) are valid, so first(n), lazy and each_slice(2).first work, while to_a and map never return

Struct

each member value

map and friends work on the values; select/filter, to_a and to_h are overridden; each_pair yields name and value

Data

not Enumerable

use to_h or deconstruct_keys; members lists the names

Set

each element

overrides only include?/member?, to_a and to_set, so select, map and sort return Arrays; the set operators and the bang methods (select!, reject!) work on the Set itself

IO, File, Dir, ARGF

a line (IO) or an entry (Dir)

File.foreach(path).lazy reads large files lazily; IO#each_line is the each

ENV

a [name, value] pair

ENV is an object that extends Enumerable, so ENV.map and ENV.select work on pairs

# 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.

  1. 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, sum and each_slice have none of them.

  2. Scope. The variable of a for loop is not local to it, so it leaks into the surrounding method, whereas a block parameter does not. A for loop is implemented with each, so it is not a faster alternative.

  3. A result. map, select and sum are expressions that produce the value, where a loop needs a variable that is initialised before it and mutated inside.

  4. A name for the intent. select, reject, partition and group_by say what is being done; a loop with an if makes 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]
  • 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 with reject/select. A Hash raises RuntimeError ("can’t add a new key into hash during iteration") if a key is added in each; deleting or updating existing keys is allowed.

  • The accumulator. inject needs the block to return the accumulator; each_with_object does not.

  • Endless sources. first, take, find, lazy and each_slice(n).first stop early; to_a, map, sort, sum, cycle without a count and reverse_each on an endless range never return.

  • break, next and return. break inside map ends the whole call and returns nil (or the value given to break); next ends the current block call only and supplies that element’s value; return inside a block returns from the enclosing method.

  • first versus take. Both return the leading elements; first without an argument returns one element (or nil), take(n) always returns an Array and raises ArgumentError ("attempt to take negative size") for a negative n.

  • Empty collections. min, max, min_by, inject without an initial value and first return nil; sum returns 0; all? and none? return true. State what you want in the code or pass an initial value.

  • Strings are not Enumerable. String#each_char, each_line and each_byte return enumerators, but a String does not include the module, so "abc".map is a NoMethodError; use chars.map or each_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:

  1. Use the most specific method. find instead of select.first, any? instead of select.any?, count { } instead of select { }.size, filter_map instead of select { }.map { }, min_by instead of sort_by { }.first, sum { } instead of map { }.sum, each_slice instead of manual slicing.

  2. Stop early. first, take_while, find and any? do not evaluate the rest of the collection.

  3. Go lazy with lazy when the source is very large, endless, or each stage is expensive and only the first few results matter; then first(n) or take(n).to_a pulls 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

Style/For

a for loop (prefer each)

Style/SymbolProc

a block that only calls a method on its parameter (use &:name)

Style/ItBlockParameter

a one-parameter block using _1 (use it)

Style/NumberedParametersLimit

more than one numbered parameter in a block

Style/RedundantSort

sort.first, sort_by { }.last and similar (use min, max_by)

Style/PartitionInsteadOfDoubleSelect

consecutive select and reject with the same condition

Style/ReduceToHash

each_with_object or inject that only builds a Hash (use to_h { })

Style/EachWithObject

inject that always returns the accumulator

Style/HashEachMethods

each over a Hash that ignores the key or the value (use each_key or each_value)

Style/ExplicitBlockArgument

a method that only yields what a block would (use &block)

Style/MapIntoArray

each that appends to an Array (use map)

Style/SelectByRegexp

select { } with a regular expression (use grep)

Style/CollectionCompact

custom logic that rejects nil (use compact)

Lint/UnmodifiedReduceAccumulator

an inject block that does not return the accumulator

Lint/Void

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

Enumerable#sum and Enumerable#uniq added; chunk without a block returns an Enumerator

2.5

any?, all?, none? and one? accept a pattern argument

2.6

Enumerable#chain and Enumerator#+ added; to_h accepts a block; filter becomes an alias of select

2.7

Enumerable#filter_map and Enumerable#tally added; numbered block parameters (_1) added

3.0

a block-less grep and grep_v with a Regexp no longer modify Regexp.last_match

3.1

Enumerable#compact added; tally accepts a Hash; each_cons and each_slice return the receiver

3.2

Set is autoloaded and Enumerable#to_set no longer needs require "set"

3.4

the it block parameter is available; Hash#inspect shows {name: "Ico"} and {"a" => 1}

4.0

Array#find and Array#rfind are native; arguments to Enumerable#to_set are deprecated; Enumerator.produce takes size:; Set is a core class

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 _1 and _2 are the short form of a block parameter

use it (Ruby 3.4) for a single parameter; _1/_2 still work

undefined method `map' for nil:NilClass

undefined method 'map' for nil (receiver description since Ruby 3.3, straight quotes since 3.4)

hash output such as {:name=>"Ico"}

{name: "Ico"} and {"a" => 1} since Ruby 3.4 (Hash#inspect)

Set needs require "set" and is a standard library class

Set is autoloaded from 3.2 and a core class from 4.0; SortedSet is gone

each_slice and each_cons return nil after a block

they return the receiver since Ruby 3.1

Enumerable#sum needs inject(:+)

sum exists since 2.4, is more precise for Floats and takes a block

a for loop is a "C-style" loop

it is each with a leaking variable; avoid it

Benchmark and Logger, used to time loops, are part of the standard library

benchmark and logger are bundled gems in Ruby 4.0 and need a Gemfile entry under Bundler

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 Enumerable methods over index-based loops, and the most specific method over a chain of general ones.

  • Give a collection class each first, then include Enumerable. Add <=> to the elements if they must sort.

  • Return an Enumerator from each when 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 inject and sum when the element type is not Integer.

  • Use lazy for large or endless sources, each_slice for batching and each_cons for neighbours.

  • Do not mutate a collection while iterating over it; iterate a copy or build a new collection.

  • Remember which methods return what: Hash#select returns a Hash, Hash#map and Set#map return an Array.