Enumerable in depth

Ruby · Practical · 14 min

Idiomatic Ruby replaces most hand-written loops with one well-chosen Enumerable method.

Enumerable is a module of 50+ methods available on any class that defines each, including Array, Hash, Range, Set and your own collections. Knowing the catalogue is what makes Ruby code short.

  • Aggregation: sum, count, min_by, max_by, minmax, and reduce(:+) or inject(0) { |acc, x| acc + x } for anything custom.
  • Grouping: group_by { |w| w.length } returns a hash of arrays; partition splits into two arrays (matches, non-matches); tally counts occurrences; chunk_while and slice_when split runs.
  • Building: each_with_object({}) { |x, h| ... } passes the same accumulator to every call, and to_h { |x| [key, value] } builds a hash directly.
  • Transforming: flat_map, filter_map (map and drop nils/falses in one pass), zip, each_slice(n), each_cons(n), uniq.
  • Ordering: sort_by { |u| [u.last, u.first] } sorts by several keys; sort uses <=>; max(3) returns the three largest.

Hashes enumerate [key, value] pairs, so blocks can take |k, v|. They also have hash-returning versions: transform_values, transform_keys, filter_map, select, sum, to_h.

When a method takes a block and you call it without one, many return an Enumerator, so you can chain: each_with_index.map { |x, i| ... } or map.with_index(1).

Example

words = %w[apple bob cat avocado banana cherry bob]

p words.tally                       # {"apple" => 1, "bob" => 2, "cat" => 1, ...}
p words.group_by(&:size)            # {5 => ["apple"], 3 => ["bob", "cat", "bob"], ...}
short, long = words.partition { it.size <= 3 }
p short                             # ["bob", "cat", "bob"]

index = words.uniq.each_with_object(Hash.new { |h, k| h[k] = [] }) do |w, h|
  h[w[0]] << w
end
p index["a"]                        # ["apple", "avocado"]

p words.uniq.sort_by { [-it.size, it] }.first(2)   # ["avocado", "banana"]
p (1..10).reduce(:*)                # 3628800
p [1, nil, 3].filter_map { it && it * 2 }  # [2, 6]
p words.map.with_index(1) { |w, i| "#{i}.#{w}" }.first(2)  # ["1.apple", "2.bob"]
prices = { tea: 3, cake: 5 }
p prices.transform_values { it * 2 }   # {tea: 6, cake: 10}
p prices.sum { |_, v| v }              # 8

Remember

  • Any class with each can include Enumerable and get 50+ methods
  • group_by, partition, tally and each_with_object replace manual loops
  • Block-less calls return Enumerators, enabling map.with_index and chaining

Watch out for

  • Using inject with a mutable accumulator but forgetting to return it from the block; use each_with_object instead.

Check yourself

  1. What does [1, 2, 3, 4].partition(&:even?) return?
  2. Why does inject({}) { |h, x| h[x] = 1 } fail on the second element?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.ruby-lang.org/en/3.4/Enumerable.html