A Ruby Hash keyed by word-pair arrays stops finding entries after the key arrays are modified; why, and what do rehash and compare_by_identity change?
answer
- slot chosen by key.hash at insert
- mutating a key changes its hash
- rehash rebuilds the index
- String keys are copied and frozen
- compare_by_identity: same object only
basics
~20 sA key is filed under its hash value when inserted; mutating an array key changes that value, so lookups search the wrong place and miss. rehash rebuilds the index from current values. compare_by_identity matches keys by object identity instead of hash and eql?.
solid answer
~40 sA `Hash` finds a key by its `hash` value and confirms it with `eql?`. An `Array`'s `hash` depends on its elements, so `pair << "twist"` after `counts[pair] += 1` leaves the entry filed under the old value: `counts[pair]` and `counts.key?(pair)` miss, although `counts.keys` still shows the mutated array. `counts.rehash` recomputes every key's hash and returns `self`; it raises `RuntimeError` if called while the hash is being iterated. String keys avoid this because an unfrozen `String` key is duplicated and frozen on insert. The better fix is keys that do not change, such as frozen arrays. `compare_by_identity` switches the hash to matching the **same object**: it ignores `hash` and `eql?`, so two equal but separately built strings become two keys, and there is no method to switch it back.
code
ruby · 19 linescounts = Hash.new(0)
pair = ["great", "plot"]
counts[pair] += 1
pair << "twist" # mutates the key object itself
counts[pair] # => 0, looked up under the new hash value
counts.key?(pair) # => false
counts.rehash
counts[pair] # => 1
word = String.new("dune")
by_word = {word => 1}
word << "!"
by_word["dune"] # => 1, the key was a frozen copy
seen = {}.compare_by_identity
seen[String.new("dune")] = 1
seen[String.new("dune")] = 2
seen.size # => 2, two distinct objectsgo deeper
Recall that mutating an array used as a hash key can make it impossible to find, and that String keys are copied and frozen.
Explain that lookups use hash then eql?, why a changed hash value misses, and what rehash and compare_by_identity each do.
Diagnose missing entries whose keys still appear in keys as mutated-key bugs, fix them by freezing keys, and keep rehash as a repair.
Set rules for what may serve as a hash key in shared code, favouring immutable value objects over repair calls scattered through callers.
## How a Hash finds a key A Ruby `Hash` stores each entry under the key's **`hash` value**, an integer the key computes from its contents. A lookup computes `key.hash` again, goes to the matching place in the table and confirms the candidate with **`eql?`**. Two objects are the same key when their `hash` values match and they are `eql?`. For an `Array` key, both `hash` and `eql?` depend on the **elements**. That is what makes `counts[["great", "plot"]]` find an entry stored under a different array with the same contents, and it is also what breaks when the contents change. ## Mutating a key breaks lookups Counting word pairs in book reviews with array keys: 1. `pair = ["great", "plot"]` and `counts[pair] += 1` store the entry under `pair.hash` as it is now. 2. `pair << "twist"` mutates **the same array object** the hash holds as its key. 3. `counts[pair]` computes the new `pair.hash`, looks in the wrong place and misses, returning the default. 4. `counts.key?(pair)` is `false`, yet `counts.keys` still lists the array, now three elements long. Nothing raises. The entry is simply unreachable by lookup until the index is repaired or the key is restored. ## rehash `Hash#rehash` rebuilds the table by recomputing every key's `hash`, then returns `self`. After it, lookups by the mutated key succeed. Two limits: - It raises `RuntimeError` (`rehash during iteration`) if called while the same hash is being iterated, for example inside its own `each`. - If two keys become `eql?` after mutation, they collapse into one entry during the rebuild. `rehash` is a repair, not a design. Prefer keys that cannot change: freeze arrays before using them as keys, or use immutable value objects. ## Why String keys are safe The Hash documentation calls a `String` key "always safe": when an **unfrozen String** is used as a key, the hash stores a **duplicated, frozen** copy. Appending to the original variable afterwards changes the variable's string, not the key. Arrays and other mutable objects get no such treatment. ## compare_by_identity | Behaviour | Default hash | After `compare_by_identity` | |---|---|---| | Same key when | `hash` equal and `eql?` | the very same object | | Two equal strings built separately | one key | two keys | | Unfrozen String keys | duplicated and frozen | stored as given | | Mutating a key object | breaks lookups | lookups still work | `compare_by_identity` returns `self`, and `compare_by_identity?` reports whether it is on. There is no method to switch back; calling it again leaves the hash as it is. Like `rehash`, it raises `RuntimeError` if called during iteration. It fits questions about **specific objects**: which objects a graph walk has already visited, or per-instance caches, where two equal but distinct objects should count separately. ## Freezing keys in practice Freezing turns the silent miss into a loud failure at the point of mutation: ```ruby pair = %w[great plot].freeze counts[pair] += 1 pair << "twist" # raises FrozenError ``` `freeze` is shallow, so the strings inside the array must not be mutated either; they are safe when they are themselves frozen or never modified. Value objects built with `Data.define` are frozen once created (shallowly, like any freeze) and define `hash` and `eql?` from their members, which makes them natural composite keys, for example `Bigram = Data.define(:first, :second)`. ## Production guidance - Freeze or copy composite keys before inserting them. - Treat a lookup that misses for a key visible in `keys` as a mutated-key symptom. - Use `compare_by_identity` only when identity is the actual question, never as a workaround for mutated keys.
- Why doesn't mutating a String variable used as a key break the hash the same way?When an unfrozen `String` is inserted as a key into a normal hash, Ruby stores a duplicated, frozen copy. Mutating the original variable changes only the variable's string, so lookups with the original contents still work. An identity hash skips this copying and stores the string as given.
- Can you call rehash inside an each loop over the same hash?No. `rehash` raises `RuntimeError` with `rehash during iteration` while the hash is being iterated. Finish the loop first, or collect the changes and apply them afterwards, then call `rehash` once.
- When is compare_by_identity the right tool?When the question is about specific objects rather than values: tracking which nodes a traversal has already visited, or caching results per instance. Equal but distinct objects then stay separate. It is the wrong tool for string or array lookups by content.
A default Hash is a cloakroom that files each coat by a description written at drop-off; repaint the coat and the attendant searches the wrong rack until the room is re-sorted, which is rehash. compare_by_identity instead ties a numbered ticket to that exact coat.
saying these in an interview costs you the question
- Ruby rehashes a key automatically when you mutate it
- Mutating a String variable used as a key breaks lookups the same way
- compare_by_identity makes two equal strings the same key
- rehash deletes the entries whose keys changed
- Calling compare_by_identity a second time switches back to eql? matching