Looking beyond records: better constructors (and deconstructors)
Brian Goetz
brian.goetz at oracle.com
Fri Jun 7 19:11:05 UTC 2019
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.
More information about the amber-spec-experts
mailing list