> 
> 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.  

Say it with me: “DAMN YOU, PERVASIVE MUTABILITY!"

> 
> 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.

Reply via email to