5

Imagine a class including comparable like this:

class Element
  include Comparable
  attr_accessor :name, :pos_x, :pos_y

  def initialize(name, pos_x, pos_y)
    @name = name
    @pos_x = pos_x
    @pos_y = pos_y
  end

  def <=>(other)
    if (@pos_x == other.pos_x) and (@pos_y == other.pos_y)
      return 0
    else 
      return @name <=> other.name
    end
  end

  def eql?(other)
    self == other
  end
end

How would you implement the hash function such that a.hash == b.hash in this case? In general I'd do:

def hash
  @name.hash
end

But this does not include pos_x and pos_y.

toro2k
  • 19,020
  • 7
  • 64
  • 71
Paddi91
  • 93
  • 5
  • 1
    I think you should think better about your equality definition, equality should be a transitive relation, yours is not. – toro2k Mar 09 '14 at 00:01

3 Answers3

1

Unfortunately this is mathematically impossible to define a valid hash function in this case.

Let a, b be two elements with equal positions, and different names. According to eql? definition, this implies that h(a) == h(b). Since this is true for any names values, the hash function is to be independent on name attribute, which is however in contradiction with the second check. Hence there is no hash function for this eql? definition. Sorry. :(

Update:

As noted by toro2k - your equality definition is not transitive. In general if a == b and b == c, it is required that a == c. According to your eql? function:

{pos_x: 1, pos_y: 1, name: 'a'} == {pos_x: 1, pos_y: 1, name: 'b'}
{pos_x: 1, pos_y: 1, name: 'b'} == {pos_x: 2, pos_y: 2, name: 'b'}

but

{pos_x: 1, pos_y: 1, name: 'a'} != {pos_x: 2, pos_y: 2, name: 'b'}

That's the root of your problem here.

BroiSatse
  • 44,031
  • 8
  • 61
  • 86
1

There is nothing you can do beyond what you are already doing. Note that your hash function is a really bad one because it has poor distribution and will lead to a lot of collisions, which will probably destroy the O(1) amortized worst-case step complexity guarantees of Hash, but there really is nothing you can do about that.

Note, however, that there is a much bigger problem with your definitions of equality and ordering: they aren't equality or ordering!

Equality and ordering relationships need to be

  • reflexive: a ~ a
  • symmetric: a ~ b <=> b ~ a
  • transitive: a ~ b && b ~ c => a ~ c

Both your equality and ordering relations violate transitivity:

one   = Element.new('Z', 1, 1)
two   = Element.new('A', 1, 1)
three = Element.new('A', 2, 2)

one == two   # => true
two == three # => true
one == three # => false # Huh?

one <= two   # => true
two <= three # => true
one <= three # => false # Huh?

All methods that use the spaceship operator (e.g. Enumerable#sort) rely on those laws. So, your <=> and eql? implementations are fundamentally broken.

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
0

You can override == instead, since Object#hash does not accept an argument:

def ==(o)
  (@pos_x == o.pos_x && pos_y == o.pos_y) || (@name == o.name)
end

alias_method :eql?, :==
Agis
  • 32,639
  • 3
  • 73
  • 81
  • I've tried this before, but it doesn't work because pos_x and pos_y can be different even when a.name == b.name. For implementation details see my spaceship function. As you can see there are actually two cases where a == b matches. – Paddi91 Mar 08 '14 at 23:05
  • @Paddi91 so you want to compare first based on pos_x and pos_y and if they are not equal then compare based on the name? Then you could just override `eql?`, not `hash`. I'll update my answer. – Agis Mar 08 '14 at 23:08
  • overriding eql? and == is what I already do, but the diff gem uses hash maps and therefore requires proper hash values for comparing the entries. – Paddi91 Mar 08 '14 at 23:27
  • I don't think there is another way if it uses `hash`. – Agis Mar 08 '14 at 23:32