I don't have an answer for you, but I can add some information to the mix.
Currently there are _nine_ "implementations" of `==`; one for
references, and one for each of the eight primitives. Regardless of
whether or not they are perfect tests of substitutibility (curse you,
floating point), the eight primitive `==` functions are highly
domain-specific. They can be so because the primitives are
monomorphic. In a sense, we've allowed primitives to "overload" `==`
because monomorphism means we can define `==` with full knowledge of the
domain, and without worry about non-well-definedness or the various
other problems of `equals` in extensible class hierarchies (as EJ
exhaustively catalogued.)
What's being proposed here is that we evolve `Object==` from "compare
identities" to a case analysis, to account for the fact that Object will
describe more things:
case (IdentityObject a, IdentityObject b) -> identity==(a, b)
case (ValueObject a, ValueObject b) -> (isNull(a) == isNull(b)) &&
(type(a) == type(b))
&& (state(a) == state(b))
default -> false
Just as `identity==` was the best we could do as a default on
polymorphic identity objects, this is the best we can do on polymorphic
mixed identity/value objects. (There's a whole digression into
overloading `==` on value types, but I'm not going to go there right now.)
While we're not making the problem of "`==` is unreliable" better, and
arguably making it incrementally worse by making it work in more cases
that look a little like the cases in which it is unreliable, we *are*
making something better here: you can now use `.equals()` everywhere.
One of the complains about `==` is that sometimes you use `==` and
sometimes you use `.equals()` and sometimes you can accidentally use one
where you should use the other. But this is because you couldn't
previous use .equals() on primitives, so an `equals()` method would
necessarily do things like:
boolean equals(Object o) {
return o instanceof Foo f
&& f.size == this.size
&& f.name.equals(this.name);
}
What stinks here is that at each point, you have to ask yourself
"equals, or =="? Now you can have a fixed rule: always say `.equals()`:
boolean equals(Object o) {
return o instanceof Foo f
&& f.size.equals(this.size) // works on int!
&& f.name.equals(this.name);
}
(The equals method on primitives is monomorphic so will JIT away, for
anyone worried about the performance.)
It is a little sad because we had to resolve the problem by using the
unfortunate spelling all the time, because `==` got the good name, but
that's not a new problem. But it means the cognitive load can disappear
if we train ourselves to uniformly use `.equals()`.
We will surely have about a million calls to make `===` or `eq` or
something else sugar for `.equals()`. We can consider that, but I don't
think its essential to do that now.
On 6/15/2022 1:51 PM, Kevin Bourrillion wrote:
What I think I understand so far:
The current plan for `==` for all bucket 2+ types (except the 8
_primitive_ types, as I still use the word) is to have it perform a
fieldwise `==` comparison: identity equality for bucket 1 fields, what
it's always done for primitive fields, and of course recurse for the rest.
If we consider that the broadest meaning of `a == b` has always been
"a and b are definitely absolutely indistinguishable no matter what",
then this plan seems to compatibly preserve that, which makes sense
for purposes of transition.
What concerns me:
It's good for transition, at least on the surface, but it's a bad
long-term outcome.
Users hunger for a shorter way to write `.equals()`, and they will
think this is it. I would not underestimate the pushback they will
experience to writing it out the long way in cases where `==` at least
*seems* to do the right thing. Because in some number of cases, it
*will* do the same thing; specifically, if you can recurse through
your fields and never hit a type that overrides equals().
This is extremely fragile. A legitimate change to one type can break
these expectations for all the types directly or indirectly depending
on it, no matter how far away.
In supporting our Java users here, there's no good stance we can take
on it: if we forbid this practice and require them to call `.equals`,
we're being overzealous. If we try to help them use it carefully, at
best users will stop seeing `Object==Object` as a code smell (as we
have spent years training them to do) and then will start misusing it
even for reference types again.
btw, why did I say it's good for transition "on the surface"? Because
for any class a user might migrate to bucket 2+, any existing calls to
`==` in the wild are extremely suspect and *should* be revisited
anyway; this is no less true here than it is for existing
synchronization etc. code.
What's an alternative?:
I'm sure what I propose is flawed, but I hope the core arguments are
compelling enough to at least help me fix it.
The problem is that while we /can/ retcon `==` as described above,
it's not behavior anyone really /wants/. So instead we double down on
the idea that non-primitive `==` has always been about identity and
must continue to be. That means it has to be invalid for bucket 2+ (at
compile-time for the .val type; failing later otherwise?).
This would break some usages, but again, only at sites that deserve to
be reconsidered anyway. Some bugs will get fixed in the process. And
at least it's not the language upgrade itself that breaks them, only
the specific decision to move some type to new bucket. Lastly, we
don't need to break anyone abruptly; we can roll out warnings as I
proposed in the email "We need help to migrate from bucket 1 to 2".
A non-record class that forgets to override equals() from Object even
upon migrating to bucket 2+ is also suspect. If nothing special is
done, it would fail at runtime just like any other usage of
`Foo.ref==Foo.ref`, and maybe that's fine.
Again, I'm probably missing things, maybe even big things, but I'm
just trying to start a discussion. And if this can't happen I am just
searching for a solid understanding of why.