Looking beyond records: better constructors (and deconstructors)

With most of the decisions regarding records being settled, let's take a few minutes to look down the road. Records are great for where they apply, but there are plenty of classes that suffer from error-prone boilerplate that do not qualify to be records. We would like for some of the record goodies to filter down to ordinary classes, where possible. The lowest-hanging fruit here is constructors: many constructors look vaguely like (or can be made to look like) this: Foo(ARGS) { if (ARGS NOT VALID) throw new IllegalArgumentException(...); if (ARGS NEED TO BE NORMALIZED / COPIED) { ARGS = normalize(ARGS); } this.ARGS = ARGS; } That is, many constructors take arguments that are candidate values for their fields, and then validate the arguments, possibly normalize or defensively copy them, and write them to the corresponding fields with the error-prone boilerplate of: this.x = x; this.y = y; Similarly, when we add deconstruction patterns, a deconstructor will likely have the similar idiom, in reverse: x = this.x; y = this.y; Records sidestep this because we have already committed to a deterministic relationship between the public construction/deconstruction protocol and the internal representation. And records let you skip the initialization boilerplate, even if you have an explicit constructor: record Range(int low, int high) { public Range { if (low > high) throw new IAE("Bad range: [%d, %d]".formatted(low, high)); // Implicit field initialization FTW! } } The author provides the explicit validity check, but the compiler fills in the boilerplate field initialization. Now, the constructor only contains the "non-obvious" code. Can we share this with ordinary classes? What we would need is to tell the compiler that the constructor argument "int low" and the field "int low" are describing the same thing. This is commonly the case, but purely convention. We could, through a variety of syntactic indicators, capture this relationship. (Please, let's decide whether we like the feature before we bikeshed the syntax.) For example: class Foo { private int x; public Foo(this int x) { } } where `this int x` means that the constructor has a parameter `int x`, which corresponds to the field `int x` of the current class. The compiler can reciprocate by filling in the `this.x = x` boilerplate where needed, and the same with a deconstruction pattern: class Foo { private int x; public Foo(this int x) { } public pattern Foo(this int x) { } } If the constructor wants to do validation and/or normalization, it is like what we do with records -- we put that in the constructor, mutating the arguments if needed, and then the argument values are committed to the fields implicitly if they are DU on all paths out of the ctor. With such a feature, then the special constructor form for records: public Range { STUFF } becomes simply shorthand for public Range(this int low, this int high) { STUFF } reducing some of the "magic" associated with records.