The hash toolbox
fetch instead of brackets
h[:missing] answers nil, which then fails somewhere else. fetch raises at the line that asked, or returns a fallback when one is given. A block computes the fallback only when it is needed:
ruby -e 'h = {a: 1}; p h.fetch(:b) { :missing }'
:missing
Use fetch for data that must be there — a configuration key, a field from an API — and brackets for a lookup where nil is a valid answer.
dig into a nested hash
dig walks a chain of keys and returns nil as soon as one is missing, without raising on a nil in the middle:
ruby -e 'h = {a: {b: 2}}; p h.dig(:a, :b); p h.dig(:x, :y)'
2
nil
The second call would raise NoMethodError as h[:x][:y], because h[:x] is nil. That is the whole reason dig exists, and it is the method to use on a JSON response whose shape is not guaranteed.
merge and transform
merge returns a new hash and lets the right side win; transform_keys and transform_values rewrite one side without a loop:
ruby -e 'p({a: 1, b: 2}.merge({b: 3}))'
{a: 1, b: 3}
ruby -e 'p({a: 1}.transform_keys { |k| k.to_s })'
{"a" => 1}
merge is shallow: a nested hash on the right replaces the one on the left, it does not merge inside it. Ruby 3.4 has deep_merge in ActiveSupport, not in core; for a plain project, merge the nested level by hand.
Default values
A default makes a missing key answer something useful. Hash.new(0) is the counting idiom; a block default is the one for a hash of lists:
ruby -e 'h = Hash.new { |h, k| h[k] = [] }; h[:a] << 1; p h'
{a: [1]}
The block receives the hash and the missing key, and the assignment inside it is what stores the new list. Without that assignment the block returns a fresh array each time and nothing is kept:
ruby -e 'h = Hash.new([]); h[:a] << 1; p h[:b]'
[1]
The second line is the classic trap: every missing key answers the same array, so h[:a] << 1 also changed h[:b]. The block form with the assignment is the only safe default for a mutable value.
What to watch for
keys, values and
each keep insertion order, so a hash printed with
p is stable and easy to compare between runs.
A hash with a default that is not in the hash is not equal to a hash that
has the key: Hash.new(0)[:a] answers zero but
keys stays empty. Store the value when the caller
needs to see it.
Next: Nested arrays and hashes
Article author: Arthur Isaev