Reputation: 509
I have a hash that uses array as its key. When I change the array, the hash can no longer get the corresponding key and value:
1.9.3p194 :016 > a = [1, 2]
=> [1, 2]
1.9.3p194 :017 > b = { a => 1 }
=> {[1, 2]=>1}
1.9.3p194 :018 > b[a]
=> 1
1.9.3p194 :019 > a.delete_at(1)
=> 2
1.9.3p194 :020 > a
=> [1]
1.9.3p194 :021 > b
=> {[1]=>1}
1.9.3p194 :022 > b[a]
=> nil
1.9.3p194 :023 > b.keys.include? a
=> true
What am I doing wrong?
Update: OK. Use a.clone is absolutely one way to deal with this problem. What if I want to change "a" but still use "a" to retrieve the corresponding value (since "a" is still one of the keys) ?
Upvotes: 17
Views: 19039
Reputation: 10049
TL;DR: consider Hash#compare_by_indentity
By default arrays .hash
and .eql?
by value, which is why changing the value confuses ruby. Consider this variant of your example:
pry(main)> a = [1, 2]
pry(main)> a1 = [1]
pry(main)> a.hash
=> 4266217476190334055
pry(main)> a1.hash
=> -2618378812721208248
pry(main)> h = {a => '12', a1 => '1'}
=> {[1, 2]=>"12", [1]=>"1"}
pry(main)> h[a]
=> "12"
pry(main)> a.delete_at(1)
pry(main)> a
=> [1]
pry(main)> a == a1
=> true
pry(main)> a.hash
=> -2618378812721208248
pry(main)> h[a]
=> "1"
See what happened there?
As you discovered, it fails to match on the a
key because the .hash
value under which it stored it is outdated [BTW, you can't even rely on that! A mutation might result in same hash (rare) or different hash that lands in the same bucket (not so rare).]
But instead of failing by returning nil
, it matched on the a1
key.
See, h[a]
doesn't care at all about the identity of a
vs a1
(the traitor!). It compared the current value you supply — [1]
with the value of a1
being [1]
and found a match.
That's why using .rehash
is just band-aid. It will recompute the .hash
values for all keys and move them to the correct buckets, but it's error-prone, and may also cause trouble:
pry(main)> h.rehash
=> {[1]=>"1"}
pry(main)> h
=> {[1]=>"1"}
Oh oh. The two entries collapsed into one, since they now have the same value (and which wins is hard to predict).
One sane approach is embracing lookup by value, which requires the value to never change. .freeze
your keys. Or use .clone
/.dup
when building the hash, and feel free to mutate the original arrays — but accept that h[a]
will lookup the current value of a
against the values preserved from build time.
The other, which you seem to want, is deciding you care about identity — lookup by a
should find a
whatever its current value, and it shouldn't matter if many keys had or now have the same value.
How?
Object
hashes by identity. (Arrays don't because types that .==
by value tend to also override .hash
and .eql?
to be by value.) So one option is: don't use arrays as keys, use some custom class (which may hold an array inside).
But what if you want it to behave directly like a hash of arrays? You could subclass Hash, or Array but it's a lot of work to make everything work consistently. Luckily, Ruby has a builtin way: h.compare_by_identity
switches a hash to work by identity (with no way to undo, AFAICT). If you do this before you insert anything, you can even have distinct keys with equal values, with no confusion:
[39] pry(main)> x = [1]
=> [1]
[40] pry(main)> y = [1]
=> [1]
[41] pry(main)> h = Hash.new.compare_by_identity
=> {}
[42] pry(main)> h[x] = 'x'
=> "x"
[44] pry(main)> h[y] = 'y'
=> "y"
[45] pry(main)> h
=> {[1]=>"x", [1]=>"y"}
[46] pry(main)> x.push(7)
=> [1, 7]
[47] pry(main)> y.push(7)
=> [1, 7]
[48] pry(main)> h
=> {[1, 7]=>"x", [1, 7]=>"y"}
[49] pry(main)> h[x]
=> "x"
[50] pry(main)> h[y]
=> "y"
Beware that such hashes are counter-intuitive if you try to put there e.g. strings, because we're really used to strings hashing by value.
Upvotes: 9
Reputation: 80085
The #rehash method will recalculate the hash, so after the key changes do:
b.rehash
Upvotes: 19
Reputation: 2590
Hashes use their key objects' hash codes (a.hash
) to group them. Hash codes often depend on the state of the object; in this case, the hash code of a
changes when an element has been removed from the array. Since the key has already been inserted into the hash, a
is filed under its original hash code.
This means you can't retrieve the value for a
in b
, even though it looks alright when you print the hash.
Upvotes: 2
Reputation: 2590
As you have already said, the trouble is that the hash key is the exact same object you later modify, meaning that the key changes during program execution.
To avoid this, make a copy of the array to use as a hash key:
a = [1, 2]
b = { a.clone => 1 }
Now you can continue to work with a
and leave your hash keys intact.
Upvotes: 1
Reputation: 3915
You should use a.clone
as key
irb --> a = [1, 2]
==> [1, 2]
irb --> b = { a.clone => 1 }
==> {[1, 2]=>1}
irb --> b[a]
==> 1
irb --> a.delete_at(1)
==> 2
irb --> a
==> [1]
irb --> b
==> {[1, 2]=>1} # STILL UNCHANGED
irb --> b[a]
==> nil # Trivial, since a has changed
irb --> b.keys.include? a
==> false # Trivial, since a has changed
Using a.clone
will make sure that the key is unchanged even when we change a
later on.
Upvotes: 1