In Ruby, why does Hash.new([]) lose appended words while Hash.new(0) counts correctly, and what does a default proc change?
answer
- default is returned, not stored
- one shared default object
- += assigns, << only mutates
- block gets the hash and the key
- assign inside the block to store
basics
~20 sHash.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 sA 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 linesreviews = ["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
Recall Hash.new(0) for counting and Hash.new { |h, k| h[k] = [] } for grouping, and that Hash.new([]) is the classic trap.
Explain returned-not-stored defaults, why += stores while << does not, and which methods consult the default and which ignore it.
Watch for storing default procs turning reads into writes and growing memory, and for procs that mutate the hash from several threads.
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