Fwd: JDK 14 Preview Records constructors

Brian Goetz brian.goetz at oracle.com
Tue Jun 9 19:52:15 UTC 2020


>
> There's a few things I'm at odds with and I'd like to highlight only 
> those
> in this post and I hope you can give me some insights that led to certain
> restrictions
>
> and where I'm misunderstanding some things maybe.
>
> My main issue is mostly how different constructors work in Records 
> compared
> to Classes and the resulting inconsistency.

"Inconsistency" is a loaded word :)  You mean "restrictions", where a 
record is a special kind of well-behaved class.

These restrictions add up to the requirement that constructor chains 
should "bottom out" in the canonical constructor.  This has several 
motivations:

  - Records are the data, the whole data, and nothing but the data. If 
your alternate constructors are not merely expanding to a 
representation, and the canonical constructor is not flowing values from 
the arguments to the fields (perhaps routed through validation and/or 
normalization), you're probably not in the semantic territory to be a 
record anyway.  This restriction highlights the fact that the canonical 
constructor arguments _are_ the (candidate) field values.
  - Records give you a clean way to declare the canonical constructor 
(the compact constructor), to minimize the overhead of centralizing 
invariant checking, to maximize chance that developers will actually 
check invariants.

Your (1) is essentially normalization.  Just inline this logic into the 
constructor:

     record Ex1(int x) {
         public Ex1 {
             if (x < 0) x = 0;
         }
     }

In the compact constructor, you just mutate the parameters if you want 
to normalize them (after validation, if any.)  The resulting parameter 
values are automatically committed to the fields at the end of the 
constructor.

In your (1), the `Example1(int x, int defaultX)` exists solely to serve 
the main constructor; it would be silly to expose this as a public 
constructor.

In your (2), this is trivially replacable with constructor chaining, 
resulting in far clearer, and less error-prone, code:

     record Ex2(UUID u) {
         // implicit canonical ctor

         public Ex2() { this(UUID.randomUUID()); }
         public Ex2(String s) { this(UUID.fromString(s); }
     }

In both of these examples, your code is improved dramatically by 
following the rules, so these restrictions seem to be doing their job.

In (3), you ask about "why can't I hide the constructor and expose a 
factory instead."  I totally understand why you would like this; we've 
been trained for a while to "Prefer factory methods to constructors."  
This has been discussed pretty extensively on the EG list; most of the 
reasons that make factories better than constructors don't apply to 
records, because records are already quite restricted.  So the main 
remaining reason is "it looks more modern."  Which is not nothing, but 
the problem is, factory methods are not part of the language, they are a 
library design pattern.  It is not great for a language feature like 
records to rely on library idioms; the causality is going the wrong 
way.  So to support "Hide the constructor because I'm going to provide a 
factory, I promise", we'd need "code generation knobs."  And once you 
add one such knob, you will invariably have a dozen, and now, rather 
than being a language feature with defined semantics, you've stuck a 
crappy macro generator in your language where no one can rely on the 
resulting semantics.

In (4), you say "assignment without <this> is unnatural." With respect, 
this is just saying "Augh, the learning, it hurts!" Similarly, "looks 
nothing like Java" could be leveled against NEARLY EVERY feature we've 
added.  On day 1, it looks unnatural, because its new, but then we 
learn.  In Java 5, people said "Augh, List<String> looks like C++."  In 
Java 8, they said "x -> x + 1 looks nothing like Java."  In Java 10, 
they said "var x = e" looks like Javascript.  But, how uncomfortable are 
you with these locutions today?

I think too you've misunderstood what is going on.  You say:

> public Example4 {
> x = x + 1; // assigns field, but looks nothing <like Java>
> } 

but this is not reassigning the field!  This is merely transforming the 
parameters.  The parameers are then committed, in bulk, to the fields at 
the end.  So the compact constructor effects a (usually trivial) 
transform on the record state, _and then_ stamps that into a new record.

I think when you learn how this works, and get used to it, you'll 
probably like it better.  It surely has made much of the boilerplate go 
away, leaving the code that actually matters in the foreground.

> this.x = x + 1; // read it might be forbidden in the future 

already forbidden.  Having two ways of doing it was confusing, as your 
note demonstrates.

In your (5), you want to expose a constructor that adds an offset to X.  
Kind of a bad semantic fit for a record, but trivially expressed:

     record Ex5(int x) {
         // implicit canonical ctor

         Ex5(int x, int plus) { this(x + plus); }
     }

In all of these, you are getting caught up on: "but I must initialize 
the fields via assignment."  Let go of that, and the answer becomes 
obvious, and your code gets better.  Note that in all the examples 
above, no one had to write a field assignment.

tl;dr:  I think this is just the growing pain of "this is different, and 
different is bad."  But if you stop thinking of records as "more concise 
classes", and instead as "nominal tuple objects", this gets a lot easier 
-- and you'll probably like it.

Perhaps a better set of questions is: "will we ever get these goodies 
for regular classes too?"  We're working on it.

Cheers,
-Brian








>
> The following examples are a bit constructed and don't make logical 
> sense,
> but I hope my idea gets across :)
>
>
> 1. Canonical constructor can not call <custom constructor>, e.g.
> delegating to spezialized normalizing constructor with a constant default
> value:
>
>
> record Example1(int x) {
>
> public Example1(int x) {
>
> this(x, 0); // compile error
>
> }
>
>
> public Example1(int x, int defaultX) {
>
> this.x = x >= 0 ? x : Math.max(defaultX, 0);
>
> }
>
> }
>
>
> 2. <Custom constructors> must call canonical constructor, e.g. support
> multiple input types or when you can't reuse canonical constructor:
>
>
> record Example2(UUID uuid) {
>
> public Example2 {
>
> uuid = UUID.nameUUIDFromBytes(uuid.toString().getBytes());
>
> }
>
>
> public Example2() { // compile error
>
> this.uuid = UUID.randomUUID();
>
> }
>
>
> public Example2(String uuid) { // compile error
>
> this.uuid = UUID.fromString(uuid);
>
> }
>
> }
>
>
>
> 3. Canonical Constructor must be public, so it's not possible to have
> only static factories or it forces normalization outside of Records
> constructor:
>
>
> record Example3(UUID uuid) {
>
>
> private Example3 { // compile error
>
> }
>
>
> public Example3(String uuid) { // compile error
>
> String uuidNormalized = StringUtils.toLowerCase(uuid);
>
> try {
>
> this.uuid = UUID.fromString(uuidNormalized);
>
> catch(IllegalArgumentException e) {
>
> this.uuid = null;
>
> }
>
> }
>
>
> public static Example3 fromString(String uuid) {
>
> return new Example3(uuid);
>
> }
>
> }
>
>
> 4. Assignment without <this> in canonical constructor is very
> unnatural, e.g. use of <this> in Classes is very logical when you would
> otherwise reassign the parameter
>
> In Records however :
>
>
> record Example4(int x) {
>
> public Example4 {
>
> x = x + 1; // assigns field, but looks nothing <like Java>
>
> }
>
>
> public Example4(int x, int plus) {
>
> this(x); // hrrng
>
> x = x + plus; // reassign parameter
>
> }
>
> }
>
>
> record Example5(int x) {
>
> public Example5 {
>
> this.x = x + 1; // read it might be forbidden in the future :(
>
> }
>
>
> public Example5(int x, int plus) {
>
> this(x); // hrrng
>
> this.x = x + plus; // is compile error
>
> }
>
> }
>
>
> 5. It's not possible to define the canonical constructor with the exact
> same parameter list as the Record definition has.
>
> The simple canonical constructor without parameters should just be an 
> alias
> for that in my opinion.
>
> For symmetrie reasons (with other constructors) and clearity I would 
> like to
> define the canonical constructor with the required parameters.
>
>
> I think most of those discrepancies (especially 4) and 5)) come from the
> desire to be less verbose and the definition of records (state and only
> state) and still have some <convenience> like Classes.
>
> However the limitations and half-way implicit <magic> makes it really
> confusing. I think it would have been better to keep constructors 
> similar to
> the ones from Classes if explicitly defined.
>
> I mean if the user is already going to manually write constructors (which
> should be a special case) the few lines of assignments don't matter 
> compared
> to the rethinking it needs each time
>
> with implicits and the restrictions that follow.
>
>
> What are your thoughts?
>
>
> Kind regards,
>
>
> Simon
>
>
>
>



More information about the amber-dev mailing list