Let’s say a reference type defines its own equality and hash methods:
class Foo
property x : Int32
def initialize(@x)
end
def_equals_and_hash x
end
If we want a Hash(Foo, _)
to consider only object identity and ignore key equality, we could use Hash#compare_by_identity
:
foo1 = Foo.new(1)
foo2 = Foo.new(1)
hash = {} of Foo => String
hash.compare_by_identity
hash[foo1] = "a"
hash[foo2] = "b"
hash[foo1] = "c"
hash # => {#<Foo:0x7fdbd05dce70 @x=1> => "c", #<Foo:0x7fdbd05dce60 @x=1> => "b"}
This no longer works when the hash key is a compound type that doesn’t define object identity, for example Tuple
, because Hash
falls back to key equality on keys that don’t respond to #object_id
:
hash = {} of {Int32, Foo} => String
hash.compare_by_identity
hash[{3, foo1}] = "a"
hash[{3, foo2}] = "b"
hash[{3, foo1}] = "c"
hash # => {{3, #<Foo:0x7fb60bc76e70 @x=1>} => "c"}
We definitely don’t want Tuple#object_id
here because not all Tuple
s need object identity, and we don’t want special Tuple
support in Hash
itself either, since Tuple
keys do not necessarily compare all of their elements by identity (imagine a Tuple(Foo, Foo)
key that compares the two elements by equality and identity respectively). Finally, we want to avoid using Foo#object_id
as part of the key, since going from the key to the actual Foo
now requires a rather unsafe Pointer(Void).new(key).as(Foo)
or some other redundant mechanism.
The solution, it seems, is to use a custom record whose equality respects Foo
’s identity:
record FooKey, i : Int32, foo : Foo do
# note: redefines what is already provided by the `record` macro
def_equals_and_hash i, foo.object_id
end
hash = {} of FooKey => String
# note: `hash.compare_by_identity` not needed anymore
hash[FooKey.new(3, foo1)] = "a"
hash[FooKey.new(3, foo2)] = "b"
hash[FooKey.new(3, foo1)] = "c"
hash # => {FooKey(@i=3, @foo=#<Foo:0x7f9775cfae70 @x=1>) => "c", FooKey(@i=3, @foo=#<Foo:0x7f9775cfae60 @x=1>) => "b"}
The main drawback is that you might need a different record type for each possible compound key that you want to use. Also FooKey
cannot be destructured by multiple assignment (each { |(i, foo), value| }
).
Is this indeed the best practice? Do you think anything can be done to improve this situation?