Records: construction and validation

Vicente Romero vicente.romero at oracle.com
Mon Mar 12 18:45:25 UTC 2018



On 03/12/2018 01:48 PM, Brian Goetz wrote:
> Here's a sketch of where our thinking is right now for construction 
> and validation.
>
> General goal: As Kevin pointed out, we should make adding incremental 
> validation easy, otherwise people won't do it, and the result is worse 
> code.  It should be simple to add validation (and possibly also 
> normalization) logic to constructors without falling off the syntactic 
> cliff, either in the declaration or the body of the constructor.
>
> All records have a /default constructor/.  This is the one whose 
> signature matches the class signature.  If you don't have an explicit 
> one, you get an implicit one, regardless of whether or not there are 
> other constructors.
>
> If you have records:
>
>     abstract record A(int a) { }
>     record B(int a, int b) extends A(a) { }
>
> then the behavior of the default constructor for B is:
>
>     super(a);
>     this.b = b;
>
> If you want to provide an explicit constructor to ensure, for example, 
> that b > 0, you could just say it yourself:
>
>     public B(int a, int b) {
>         if (b <= 0)
>             throw new IllegalArgumentException("b");
>         super(a);
>         this.b = b;
>     }
>
> Wait, wait a second...  I thought we couldn't put statements ahead of 
> the super-call?
>
> DIGRESSION...
>
> Historically, this() or super() must be first in a constructor. This 
> restriction was never popular, and perceived as arbitrary. There were 
> a number of subtle reasons, including the verification of 
> invokespecial, that contributed to this restriction.  Over the years, 
> we've addressed these at the VM level, to the point where it becomes 
> practical to consider lifting this restriction, not just for records, 
> but for all constructors.
>
> Currently a constructor follows a production like:
>
>     [ explicit-ctor-invocation ] statement*
>
> We can extend this to be:
>
>     statement* [ explicit-ctor-invocation statement* ]
>
> and treat `this` as DU in the statements in the first block.
>
> ...END DIGRESSION
>
> OK, so we can put a statement ahead of the super-call.  But this 
> explicit declaration is awfully verbose.  We can trim this by:
>  - Allow the compiler to infer the signature for the default 
> constructor, if none is provided;
>  - Provide a shorthand for "just do the default initialization".
>
> Now we get:
>
>     public B {
>         if (b <= 0)
>             throw new IllegalArgumentException("b");
>         default.this(a, b);
>     }
>
> There's still some repetition here; it would be nice if the default 
> initialization were inferred as well.  Which leads to a question: if 
> we have a record constructor with no explicit constructor call, do we 
> do the default initialization at the beginning or the end?  In other 
> words, does this:
>
>     public B {
>         if (b <= 0)
>             throw new IllegalArgumentException("b");
>     }
>
> mean
>
>     public B {
>         if (b <= 0)
>             throw new IllegalArgumentException("b");
>         default.this(a, b);
>     }
>
> or this:
>
> public B {
> default.this(a, b);
> if (b <= 0)
>             throw new IllegalArgumentException("b");
>     }
>
> The two are subtly different, and the difference becomes apparent if 
> we want to normalize arguments or make defensive copies, not just 
> validate:
>
> public B {
>         if (b <= 0)
>             b = 0;
>     }
>
> If we put our implicit construction at the beginning, this would be a 
> dead assignment to the parameter, after the record was initialized, 
> which is almost certainly not what the user meant. If we put it at the 
> end, this would pick up the update.  The former seems pretty 
> error-prone, so the latter seems attractive.
>
> However, this runs into another issue, which is: what if we have 
> additional fields?  (We might disallow this, but we might not.) Now 
> what if we wanted to do:
>
>     record B(int a, int b) {
>         int cachedSum;
>
>         B {
>             cachedSum = a + b;
>         }
>     }
>
> If we treat the explicit statements as occuring before the default 
> initialization, now `this` is DU at the point of assigning 
> `cachedSum`, and the compiler tells us that we can't do this.  Of 
> course, there's a workaround:
>
> B {
> default.this(a, b);
> cachedSum = a + b;
>         }
>
> which might be good enough. (Note that we'd like to be able to extend 
> this ability to constructors of classes other than records eventually, 
> so we should work out the construction protocol in generality even if 
> we're not going to do it all now.)
>
> Is `default.this(a, b)` still too verbose/general/error-prone? Would 
> some more invariant marker ("do the default thing now") be better, like:
>
>     B {
>         new;
> this.cachedSum = a + b;
>     }
>
>
>
> So, summarizing:
>  - We're OK with Foo { ... } as shorthand for the default constructor?
>  - Where should the implicit construction go -- beginning or end?

I think that placing it at the beginning covers more useful cases: 
validation, caching, etc

>  - Should there be a better idiom other than default.this(args) for 
> "do the explicit construction now"?

bikeshed: what about default(args)?

>
>
Thanks,
Vicente


More information about the amber-spec-experts mailing list