← Reference · Nestor G Pestelos Jr · Print this page

Programming Languages · Ruby

Ruby Hash Accumulators

Reference entry · last updated September 21, 2026

A Ruby hash accumulator is a Hash used to aggregate values under keys, for example a running sum per category or a list of names per group. Because a lookup of a key that has not been stored returns the hash's default value, an accumulator must either seed each key on first sight or configure a default. Producing a fixed output order then means sorting the hash's pairs, since a Hash enumerates in insertion order rather than sorted order.[1]

First principles and definitions

Lookup and the default value

A Hash is a collection of unique keys mapped to values. A lookup with a key that is not present returns a default. With Hash.new and no argument the default is nil. With Hash.new(obj) that single object is returned for every missing key. With a block, the block is called with the hash and the key, and it is responsible for storing a value if one is wanted.[1]

This default rule is what makes an accumulator necessary. Writing totals["a"] += 3 against an empty {} first reads totals["a"], which is nil, and then attempts nil + 3, which raises. The seed step gives the first read an arithmetic starting point.

totals = {}
totals["a"] += 3   # NoMethodError: undefined method '+' for nil

Objects, references, and mutation

Ruby variables hold references to objects. Assigning the same object as a default for many keys means those keys share one object, so a mutating method called through any of them is visible through all of them. Immutable values such as integers avoid this, because no method changes them in place. The distinction between an immutable default and a shared mutable default is the source of the most common accumulator bug.[1]

Seeding an accumulator

Seeding with ||=

The ||= operator assigns a value only when the current value is nil or false. On an empty hash, a missing key reads as nil, so the first visit stores the seed and later visits keep the accumulated value. A missing key stays nil until it is assigned, so a mistyped key remains visible as nil.[1]

totals = {}
totals["a"] ||= 0
totals["a"] += 3   # => 3

totals["zzz"]      # => nil

Configuring a default with Hash.new

Hash.new(0) makes every missing key read as 0, so the seed line is not needed. Reading a missing key does not store it, so the key appears in the hash only once a value is assigned through += or =. This is the shorter form and the conventional choice for integer counters.[1]

totals = Hash.new(0)
totals["a"] += 3   # => 3
totals             # => {"a"=>3}

totals["zzz"]      # => 0
totals.keys        # => ["a"]

The tradeoff is that the default applies to every absent key. A mistyped key reads as 0 instead of nil, so the mistake is silently absorbed into the result.

The shared default trap

A mutable default object is shared by every missing key. Hash.new([]) therefore hands the same array to each lookup, and appending to it through one key changes what every other key sees, while storing no key at all. The block form avoids this by creating and storing a fresh value per key on first access.[1]

groups = Hash.new([])
groups["a"] << 1
groups["b"] << 2
groups["a"]        # => [1, 2]
groups.keys        # => []

groups = Hash.new { |hash, key| hash[key] = [] }
groups["a"] << 1   # => [1]
groups.keys        # => ["a"]

Hash(0) is not a default constructor at all. Kernel#Hash converts an argument by calling to_hash, and an integer has no such method, so it raises TypeError.[2]

Ordering the result

A Hash enumerates its entries in insertion order, which is the order in which keys were first stored. That order is not sorted order. To return pairs in a chosen order, call sort, which comes from Enumerable and returns an Array of the entries, not a Hash. For a hash each entry is a two element [key, value] pair.[1][3]

h = {}
h["z"] = 1
h["a"] = 2
h.keys              # => ["z", "a"]

h.sort              # => [["a", 2], ["z", 1]]
h.sort.to_h         # => {"a"=>2, "z"=>1}

Pairs sort by their elements in order. Array#<=> compares the first element of each array, and only when those are equal does it compare the second.[4] That single rule is what makes a composite sort key work: the first element is the primary key and the second is the tiebreak.

transform_values(&:sort) is a different operation. It transforms each value rather than ordering the entries, so it is meaningful only when the values are themselves collections. Applied to integer values it raises NoMethodError.[5]

Choosing a seed

References

  1. ^ Ruby 2.6.10 core documentation, "Hash," including Hash.new, defaults, and insertion order. Free full text: ruby-doc.org/core-2.6.10/Hash.html
  2. ^ Ruby 2.6.10 core documentation, "Kernel," Hash(arg). Free full text: ruby-doc.org/core-2.6.10/Kernel.html
  3. ^ Ruby 2.6.10 core documentation, "Enumerable," sort. Free full text: ruby-doc.org/core-2.6.10/Enumerable.html
  4. ^ Ruby 2.6.10 core documentation, "Array," <=> element-wise comparison. Free full text: ruby-doc.org/core-2.6.10/Array.html
  5. ^ Ruby 2.6.10 core documentation, "Hash," transform_values. Free full text: ruby-doc.org/core-2.6.10/Hash.html