I'm wondering how could pitfall #4 in this link be avoided without runtime type (or class ID etc.) checks: https://www.artima.com/articles/how-to-write-an-equality-method-in-java.
From a language designer's point of view, the problem is the following:
We want to add an equals
(or operator ==
) method in a system with subtyping. It should be possible for users to implement it for their types.
The method should have the usual properties:
- Reflexivity:
a == a
should betrue
, for anya
- Symmetry: if
a == b
istrue
, thenb == a
should betrue
. - Transitivity: if
a == b
istrue
andb == c
istrue
,a == c
should betrue
.
We have a few options on where and how to add the method:
- We can do as some of the mainstream OOP languages do and add a
bool equals(Object other)
to theObject
type. (whereObject
is the top type) - We can have a separate class/abstract class/interface:
abstract class Equals { bool equals(Self other); }
. - We can have something like a trait or typeclass:
trait Equals { bool equals(Self other) }
.
Regardless of how and where we add this, user-written implementations will inevitably break symmetry, and probably transitivity as well.
As an example, let's consider the same example as in the link:
class Point {
int x;
int y;
}
class ColoredPoint extends Point {
Color color;
}
Regardless of how we implement equality on these types, it will be possible to call point == coloredPoint
(where point : Point
and coloredPoint : ColoredPoint
), which will compare x
and y
and return true
in some cases, but coloredPoint == point
will always return false
.
The ways shown in the post to fix this require class ID or runtime type checks (like instanceof
), and even then they break substitutability (e.g. a value of ColoredPoint
cannot be passed where Point
is expected as it behaves differently).
So it seems like something fundamental needs to change, either how we model equality, or maybe some aspects of the language.
Is avoiding subtyping the only option? Can I have an alternative equality relation (maybe relaxing some of the properties without creating potential for surprising behavior) that works in data structures like hash maps?
equals
method to object is that it forces reference equality to be the default implementation. You then have anequals
method that sometimes is value equality and sometimes is reference equality. I think those ought to be distinct operations. $\endgroup$==
asa.equals(b) && b.equals(a)
- ie. have the class define only the "is equal to me from my point of view" part of equality and require the two implementations to agree on that. Whether it's actually useful... $\endgroup$ColoredPoint
to be a class that Holds aPoint
and aColor
instead of inheriting, then you can also make sure that a user is always explicitly comparing either only thePoint
or thePoint + Color
. Such a language could get some of the benefits of inheritance by allowing a "downcast" via specifying a field or property to take it's place. Then a(Point)coloredPoint
would return thePoint
field. $\endgroup$