Records: construction and validation

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