Hash Default Values
Going through challenges on Exercism, I’ve seen many cases where hashes can be initialized with a default value to remove the logic needed for “if it is set, do this, else do that”, but creating that default value can be a little confusing sometimes.
When to use Hash.new(default)
Running Hash.new(default_value)
will make a hash that instead of
returning nil
when you lookup a key that doesn’t exist,
it will return default_value
.
This can be useful for something like counting occurrences in an array.
If I have some array of letters [:a, :a, :a, :b, :b]
and want a count of every
element in the array like { a: 3, b: 2 }
,
I can calculate this with something like:
1 2 3 4 |
[:a, :a, :a, :b, :b].each_with_object({}) do |letter, counts| counts[letter] ||= 0 counts[letter] += 1 end |
In this case, we can remove the entire counts[letter] ||= 0
line by using Hash.new(0)
1 2 3 |
[:a, :a, :a, :b, :b].each_with_object(Hash.new(0)) do |letter, counts| counts[letter] += 1 end |
When not to use Hash.new(default)
As helpful as the default_value
is, it doesn’t always work as you predict it would.
Our hash will always the exact same that you give it inside the default.
For things like the counts example above,
this doesn’t matter because we’re reseting the value for a key whenever we add something.
However, if we’re trying to change the default object rather than reset the hash value,
we start getting strange behavior.
1 2 3 4 5 6 7 |
default_array = [] hash = Hash.new(default_array) hash[:some_key] << 7 hash[:some_other_key] << 8 hash # => {} default_array # => [7, 8] |
Because hash[:some_key]
is just giving us default_array
but never setting that key
in the hash, we’re changing that default object but it’s still doing nothing to the hash.
I’m just going to leave the following code, and you can look over it if you like, but I don’t suggest this. The difference here is we’re adding an array to an empty array and just creating more arrays every time we do anything.
1 2 3 4 5 6 7 |
default_array = [] hash = Hash.new(default_array) hash[:some_key] += [7] hash[:some_other_key] += [8] hash # => {:some_key=>[7], :some_other_key=>[8]} default_array # => [] |
The “REAL” way (the one I suggest) you try to get at this problem is use Hash.new(&block)
.
This works like the following:
1 2 3 4 5 6 7 8 |
Hash.new { |the_hash, key_that_was_called_but_not_set| "do something" } hash = Hash.new { |h, key| h[key] = [] } hash[:some_key] << 7 hash[:some_other_key] << 8 hash # => {:some_key=>[7], :some_other_key=>[8]} |
When using the block format, the hash will run the block whenever a key that is not set has been called. In this case, we want the new key to be set to an empty array right away. This way we have our keys set and to a different empty array every time we call a new one.
Conclusion
If you’re going to use default hash values, pretty much use Hash.new(value)
for anything you’re using +=
for (numbers, strings, etc), then Hash.new { something }
for everything else.