skip to content

In Ruby, why does Hash.new([]) lose appended words while Hash.new(0) counts correctly, and what does a default proc change?

level: middleimportance: must knowfreq 70%

answer

  1. default is returned, not stored
  2. one shared default object
  3. += assigns, << only mutates
  4. block gets the hash and the key
  5. assign inside the block to store

basics

~20 s

Hash.new(obj) returns one shared obj for any missing key and stores nothing. counts[w] += 1 works because += assigns; h[k] << x mutates the shared array and adds no key. Hash.new { |h, k| h[k] = [] } stores a fresh value per key.

solid answer

~40 s

A hash default is **returned** for a missing key, never **stored**. `counts[w] += 1` expands to `counts[w] = counts[w] + 1`: the read returns the default `0`, and the assignment stores `1`. `by_word[w] << id` on `Hash.new([])` reads the one shared default array, appends to it and never assigns, so the hash stays `{}` while every missing key now returns that array. A default proc, `Hash.new { |hash, key| hash[key] = [] }`, is called with the hash and the key on each miss; assigning inside it stores a new array per key, while a block that only returns `[]` still stores nothing. Defaults apply to `[]`, `dig` and `values_at`, not to `fetch` or `key?`, and in Ruby 4.0 `shift` on an empty hash returns `nil` whatever the default.

code

ruby · 16 lines
ruby
reviews = ["great plot", "slow plot", "great cast"]

counts = Hash.new(0)
reviews.each { |r| r.split.each { |w| counts[w] += 1 } }
counts           # => {"great" => 2, "plot" => 2, "slow" => 1, "cast" => 1}
counts["dull"]   # => 0
counts.key?("dull") # => false

shared = Hash.new([])
shared["great"] << 0
shared           # => {}
shared["other"]  # => [0], the default itself was mutated

index = Hash.new { |hash, word| hash[word] = [] }
reviews.each_with_index { |r, i| r.split.each { |w| index[w] << i } }
index["plot"]    # => [0, 1]

go deeper

for a junior

Recall Hash.new(0) for counting and Hash.new { |h, k| h[k] = [] } for grouping, and that Hash.new([]) is the classic trap.

for a middle

Explain returned-not-stored defaults, why += stores while << does not, and which methods consult the default and which ignore it.

for a senior

Watch for storing default procs turning reads into writes and growing memory, and for procs that mutate the hash from several threads.

for a principal

Decide when implicit defaults obscure data flow enough that explicit fetch fallbacks or dedicated value objects serve a codebase better.

## Two kinds of default A Ruby `Hash` decides what a missing key returns with two properties: - **`default`**, one object returned for any missing key. Set it with `Hash.new(value)` or `h.default = value`. - **`default_proc`**, a block called with the hash and the missing key. Set it with `Hash.new { |hash, key| ... }` or `h.default_proc = proc`. Passing both a value and a block to `Hash.new` raises `ArgumentError`. With neither, missing keys return `nil`. The central rule: **a default is returned, not stored**. Reading a missing key does not add it to the hash. ## Why Hash.new(0) counts correctly Counting word frequencies in book reviews is the textbook use: ```ruby counts = Hash.new(0) review.split.each { |word| counts[word] += 1 } ``` `counts[word] += 1` is shorthand for `counts[word] = counts[word] + 1`: 1. `counts[word]` misses and returns the default `0`. 2. `0 + 1` computes a new `Integer`. 3. `counts[word] = 1` **stores** the key. The shared default `0` is immutable, so nothing can corrupt it. ## Why Hash.new([]) loses data Grouping review ids by word with `Hash.new([])` goes wrong: 1. `by_word["great"]` misses and returns the default array, the **same object** for every key. 2. `<< 1` appends to that shared array in place. 3. No assignment happens, so `by_word` is still `{}`. 4. `by_word["anything"]` now returns `[1]`, because the default itself was mutated. Ruby's own documentation warns that using a mutable object as the default "may not be a good idea". ## The default proc | Construction | Missing-key read returns | Stores the key? | |---|---|---| | `Hash.new(0)` | the shared `0` | no, but `+=` assigns | | `Hash.new([])` | the one shared array | no | | `Hash.new { \|h, k\| [] }` | a fresh array each time | no | | `Hash.new { \|h, k\| h[k] = [] }` | a fresh array, now stored | yes | Only the last form makes `by_word[word] << id` work, because the block both builds a new array and assigns it. Since the block receives the key, it can also compute per-key defaults, such as `Hash.new { |h, word| h[word] = word.length }`. ## Where defaults apply - **Consulted by**: `[]`, `dig` and `values_at`. - **Ignored by**: `fetch`, `fetch_values` and `assoc`. `Hash.new(0).fetch(:x)` raises `KeyError`. - **Not a key**: `key?` returns `false` for a key that only returns a default. - **`shift`**: since Ruby 3.2, `shift` on an empty hash returns `nil` instead of the default or the default proc's result. ## Changing defaults after creation - **`h.default = value`** sets the any-key default and **clears** any default proc; the two share one slot, so a hash has at most one of them. - **`h.default_proc = proc`** installs a per-key default and replaces the default value. `h.default_proc = nil` removes it. - A **lambda** used as a default proc must accept exactly two arguments, the hash and the key; otherwise the assignment raises `TypeError` (`default_proc takes two arguments`). - `default` and `default_proc` read the current settings, which helps when debugging a hash built elsewhere. ## Side effects of a storing proc A default proc that assigns turns **reads into writes**. `if by_word["zzz"].empty?` inserts `"zzz" => []`, so lookups used as checks grow the hash; use `key?` or `fetch` to check. The Hash documentation also notes that a proc modifying the hash is **not thread-safe**: two threads can run it for the same key at once, and one array can overwrite the other. Since Ruby 3.4, `Hash.new(capacity: 10_000)` preallocates room for a known number of entries; it combines with a default value or a block and does not change any of the rules above.

  • What goes wrong with Hash.new { |h, k| [] }, a block that does not assign?
    Each miss returns a fresh array, so there is no sharing, but nothing is stored. `h[:plot] << 1` appends to a throwaway array and the hash stays `{}`. The block must assign, `h[k] = []`, for the value to become part of the hash.
  • How can a storing default proc make a simple read change the hash?
    With `Hash.new { |h, k| h[k] = [] }`, reading `h["zzz"]` calls the proc, which inserts `"zzz" => []`. Code that uses `h[word].empty?` as a membership test therefore adds every word it checks. Use `key?` or `fetch` to check without inserting.
  • Is a default proc that assigns safe when several threads read the hash?
    Ruby's Hash documentation says no: modifying the hash from its default proc is not thread-safe, and two threads can call the proc for the same key concurrently, so one new array can replace the other and lose appends. Build per-thread hashes and merge them, or synchronise access.

saying these in an interview costs you the question

  • Hash.new([]) creates a new empty array for each missing key
  • Reading a missing key from Hash.new(0) stores the key with 0
  • Hash.new { |h, k| [] } stores the new array in the hash
  • fetch returns the default set by Hash.new
  • key? returns true for any key that returns a default
  • counts[w] += 1 on Hash.new(0) raises NoMethodError for a new word