Collections, blocks and Enumerable

Array, Hash and Set in practice, the Enumerable methods you will use daily, lazy enumerators, symbols versus strings, and destructive methods.

Array, Hash and Set

users = [{ id: 1, name: "Ada", age: 36 }, { id: 2, name: "Grace", age: 45 }]

users.map { |u| u[:name] }                 # => ["Ada", "Grace"]
users.select { |u| u[:age] > 40 }          # subset
users.partition { |u| u[:name].start_with?("A") }
users.sum { |u| u[:age] }
users.group_by { |u| u[:age] / 10 * 10 }   # => {30=>[...], 40=>[...]}
users.min_by { |u| u[:age] }
users.index_by { |u| u[:id] }              # ActiveSupport; Hash#[]= equivalent below

index = users.each_with_object({}) { |u, acc| acc[u[:id]] = u }

counts = Hash.new(0)                       # default value, not a default object
%w[a b a].each { |w| counts[w] += 1 }      # => {"a"=>2, "b"=>1}

require "set"
seen = Set.new
seen << "a"
seen.include?("a")                         # O(1), unlike Array#include?
  • each_with_object is the idiomatic accumulator: the memo is the block's second argument and is returned.
  • Hash.new(0) uses an immutable default; Hash.new { |h, k| h[k] = [] } is needed when the default must be mutable, otherwise every missing key shares one array.
  • Since Ruby 1.9 a Hash preserves insertion order, so iteration order is deterministic and testable.
  • Converting an Array to a Set turns an O(n) membership test inside a loop into an O(1) lookup - the single easiest performance win in Ruby.
💡
Naming signals intent: a trailing ! means a destructive or surprising variant (sort! mutates, sort returns a new array), and a trailing ? means a boolean. Nothing enforces this, so honouring it is what makes Ruby readable.

Blocks, procs and lazy enumerators

def each_chunk(list)
  return to_enum(:each_chunk, list) unless block_given?
  list.each_slice(100) { |slice| yield slice }
end

adder = ->(a, b) { a + b }        # lambda: strict arity, returns from itself
doubler = proc { |x| x * 2 }      # proc: lenient arity, returns from the method

adder.call(1, 2)
adder.(1, 2)
[1, 2, 3].map(&:to_s)             # Symbol#to_proc shorthand

# lazy: the pipeline stops when the consumer stops asking
(1..Float::INFINITY).lazy
  .map { |n| n * n }
  .select { |n| n.even? }
  .first(5)                        # => [4, 16, 36, 64, 100]

File.foreach("huge.log").lazy
    .select { |line| line.include?("ERROR") }
    .take(10)
    .to_a
ConstructArityreturn means
lambda / ->StrictReturn from the lambda
procLenient (fills nil)Return from the enclosing method
Block passed with yieldLenientReturn from the enclosing method
Symbol#to_procOne argumentn/a

A method that wants to accept a block should either call yield or capture it explicitly with &block. To make it return an enumerator when no block is given, use return to_enum(__method__, args) unless block_given?.

Traps worth knowing

a = [1, 2, 3]
b = a                        # same object
c = a.dup                    # shallow copy
d = a.map(&:itself)          # new array, new values for immutables

prices = { "tea" => 3, "cake" => 5 }
prices.each { |pair| }       # pair is the [key, value] array
prices.each { |k, v| }       # destructuring, the form you usually want

words = %w[banana Apple cherry]
words.sort                   # ["Apple", "banana", "cherry"] - byte order, uppercase first
words.sort_by(&:downcase)    # case-insensitive

[1, 2, 3].each_with_index.map { |n, i| n * i }
[[1, 2], [3, 4]].to_h
[nil, 1, nil].compact
  • dup and clone copy the container, not its elements - nested hashes are still shared.
  • String#== compares content while equal? compares object identity; :symbol == "symbol" is false.
  • freeze a constant such as STATES = %w[...].freeze; an unfrozen array constant can be mutated by any caller.
  • Prefer each plus a clear accumulator over chained map and flatten - each intermediate step allocates a full array.

FAQ

Symbols or strings for hash keys?
Symbols for fixed internal keys, because they are interned and cheaper to compare. Use strings for keys that arrive from external data such as JSON or a database row, and convert deliberately at the boundary instead of mixing both.
Why does mutating an array inside each not affect the original?
Because each yields values, not references into the container. Use map! or each_index to mutate in place, and be aware that mutation during iteration produces surprising results.

Syntax and blocks Strings, files and input/output

Last refreshed 2026-09-18.