Concerns about the plan for `==`

Brian Goetz brian.goetz at oracle.com
Wed Jun 15 18:14:36 UTC 2022


The way I would interpret this is "Great, you just made == slightly more 
reliable, but *still unreliable*.  Now people will be even more likely 
to make mistakes with it."  Right?

The basic problem stems from that fact that users want to think in terms 
of an `eq` operation, and the language needs a "primitive" equals which 
gets used at the base of the tower of overloaded equals() methods, but 
the language has given the good name to the thing that users usually 
don't want.  (Javascript bit the bullet and introduced `===` for this 
reason, though they had managed to bork up equality much more 
dramatically before they pulled this ripcord.)

Let's also observe that there is currently not just one `==` operation, 
but nine; there is one for object references, and one for each primitive 
type, each with its own ad-hoc meaning.  If Valhalla is to deliver on 
the promise of "programmable primitives", not being able to compare "new 
primitives" with `==` doesn't feel like "works like an int."

The obvious first counter-argument here is "then let me overload `==` 
for primitives, because otherwise, I can't even write classes like 
`float` which treat 0.00 as == to -0.00.  To which I might say "I agree, 
we need to address operator overloading, at least for user-definable 
numerics, anyway", at which point the current `==` is just the default 
for classes that don't override `==`.

At which point you counter again with "Great, so I can do that for 
identity classes too?"  At which point we have to face the real problem.

The root problem of `==` and `.equals()` is that *Object::equals was 
fundamentally the wrong design.*  Josh spends half a dozen Items in EJ 
about "how not to shoot your foot with equals".  And this comes from two 
roots: equals never should have been an instance method in the first 
place, and we want to be able to say that instances of C can sometimes 
be equals to instances of subclasses of C, under the right conditions.  
This is not a stable design.  (An example of a more stable design is 
where equality is a witness to `Eq t`, where both sides have to agree on 
the definition before they can be compared.)  But that runs into the 
desire for equality across subclasses.  The design for equality, in the 
context of extensible classes, is on a collision course with reality.

What this says is that `==` for identity classes will remain a permanent 
toxic waste zone, but it can be made to behave well -- in fact, the way 
people want -- for values.  Which is truly a glass half everything; the 
fact that we can get what we want on one side, makes it more galling 
that we can never get what we want on the other side.

We can choose to drain the glass preemptively to avoid regret, or fill 
the glass halfway with something good now (and better later), with the 
understanding that this glass will not be fully filled.  Both choices 
suck.  (As does papering over both with `===`.)

How's your day going?





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.
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/valhalla-spec-observers/attachments/20220615/9585428f/attachment-0001.htm>


More information about the valhalla-spec-observers mailing list