Updated document on data classes and sealed types

Brian Goetz brian.goetz at oracle.com
Fri Mar 8 09:04:39 UTC 2019

> Okay, mutable field types happen, and I can't think of any other reasonable approach besides allowing accessor overrides.

Yes, shaking your first and shouting “damn you, pervasive mutability” into the void (because that’s where the side effects live, of course) is a perfectly rational response.  

> There’s another consideration, too.  We considered outlawing overriding the equals/hashCode method.  This goes a long way towards enforcing the desired invariants, but again seems pretty restrictive.
> Yes, it's restrictive, and restrictiveness is what's great about records. Is this decision based on compelling use cases or on "seems pretty restrictive”?

Now that you are caught in the grasp of pervasive mutability, here’s another shake.  

For a well-behaved record R(x,y) and an instance `r`, it seems pretty reasonable to expect that new R(r.x(), r.y()) equals r.  If we have

     record KevinHatesLife(int[] ints) { }

and we take the default accessor and equals, this will be true, but we’ll be leaking our mutability.  There exist cases where that’s OK and desired, but there are cases when we want to not be leaky.  So we override the ints() accessor to clone on the way out.  But now, when we deconstruct-and-reconstruct, the new KHL is not equal to the old one.  To restore the desired equality semantics, we want to override equals as follows:

    boolean equals(Other o) { 
        return (o instanceof KevinHatesLife(var is)) && Arrays.equals(ints, is);

(and of course the same for hashCode.)  Seems mean to not let you define equality in this way.  


> I'm a bit relieved to hear this. The document seemed to imply that you would assign to the field, then later the remaining fields that weren't DA would be set from the remaining parameters. I think parameter reassignment is superior because there are never two versions of the data in scope at the same time. (I think the argument against parameter reassignment is mainly that it's heresy.)

Yes!  While it may seem more “efficient” to just write to the field directly, it would make things much more complicated.  If our ctor was

     Foo { 
          if (x < 0) 
              this.x = 0;

now on exit from the ctor, this.x is neither DA nor DU, so the compiler would have to generate some pretty nasty code to replicate the conditions under which it would want to do the assignment.  So either the user-written ctor always writes the field (DA), or never does (DU) — or it’s an error.  

> The stricture against derived fields was probably the hardest choice here.  On the one hand, strictly derived fields are safe and don’t undermine the invariants; on the other, without more help from the language or runtime, we can’t enforce that additional fields are actually derived, *and* it will be ultra-super-duper-tempting to make them not so.  (I don’t see remotely as much temptation to implement maliciously nonconformant accessors or equals methods.)  If we allowed additional fields, we would surely have to lock down equals/hashCode.
> I still want to understand what the scenario we're worried about here is. Whether the value is computed later using a "lazy fields" feature or eagerly in the constructor, only the record's state is in scope, and sure, people can shoot themselves in the foot by calling out to some static method and getting some result not determined by the parameters, but why is this worth worrying about? Do you have an example that's both dangerous and tempting? (Sorry if you've said it before.)

If we did allow them, the next thing people (Alan already did, and you made this same comment in an earlier round) would ask is whether they can be mutable — so that derived fields can be lazily derived,  Now, records are a combination of a “true record" plus an unconstrained bag of mutable state.  And what do you think the chances are that this state won’t make it into equals/hashCode semantics?   Now, we’ve completely lost our grasp on the semantic constraint — that a record is “just” its state.  We could try to put the toothpaste back in the tube by clamping down on the ability to override equals/hashCode, but now the previous example rears its head again.  

Worse, it does a lot of damage to the mental model of what records are for. We know they’re not about writing a class with fewer lines of code (though that’s an advantage), they’re about transparent carriers for a defined unit of state.  But, if users routinely see records in the wild with lots of extra state stapled to the side, maybe even affecting equals/hashCode, this design center is going to be harder to see (this ultimately leads to a feedback loop, where users who can’t understand what the feature is for will demand more features that are out of line with its design center, further obscuring the design center.)  

As I said to Alan, uncomfortable tradeoffs indeed.

More information about the amber-spec-observers mailing list