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