Fwd: JDK 14 Preview Records constructors
Brian Goetz
brian.goetz at oracle.com
Tue Jun 9 19:52:15 UTC 2020
>
> There's a few things I'm at odds with and I'd like to highlight only
> those
> in this post and I hope you can give me some insights that led to certain
> restrictions
>
> and where I'm misunderstanding some things maybe.
>
> My main issue is mostly how different constructors work in Records
> compared
> to Classes and the resulting inconsistency.
"Inconsistency" is a loaded word :) You mean "restrictions", where a
record is a special kind of well-behaved class.
These restrictions add up to the requirement that constructor chains
should "bottom out" in the canonical constructor. This has several
motivations:
- Records are the data, the whole data, and nothing but the data. If
your alternate constructors are not merely expanding to a
representation, and the canonical constructor is not flowing values from
the arguments to the fields (perhaps routed through validation and/or
normalization), you're probably not in the semantic territory to be a
record anyway. This restriction highlights the fact that the canonical
constructor arguments _are_ the (candidate) field values.
- Records give you a clean way to declare the canonical constructor
(the compact constructor), to minimize the overhead of centralizing
invariant checking, to maximize chance that developers will actually
check invariants.
Your (1) is essentially normalization. Just inline this logic into the
constructor:
record Ex1(int x) {
public Ex1 {
if (x < 0) x = 0;
}
}
In the compact constructor, you just mutate the parameters if you want
to normalize them (after validation, if any.) The resulting parameter
values are automatically committed to the fields at the end of the
constructor.
In your (1), the `Example1(int x, int defaultX)` exists solely to serve
the main constructor; it would be silly to expose this as a public
constructor.
In your (2), this is trivially replacable with constructor chaining,
resulting in far clearer, and less error-prone, code:
record Ex2(UUID u) {
// implicit canonical ctor
public Ex2() { this(UUID.randomUUID()); }
public Ex2(String s) { this(UUID.fromString(s); }
}
In both of these examples, your code is improved dramatically by
following the rules, so these restrictions seem to be doing their job.
In (3), you ask about "why can't I hide the constructor and expose a
factory instead." I totally understand why you would like this; we've
been trained for a while to "Prefer factory methods to constructors."
This has been discussed pretty extensively on the EG list; most of the
reasons that make factories better than constructors don't apply to
records, because records are already quite restricted. So the main
remaining reason is "it looks more modern." Which is not nothing, but
the problem is, factory methods are not part of the language, they are a
library design pattern. It is not great for a language feature like
records to rely on library idioms; the causality is going the wrong
way. So to support "Hide the constructor because I'm going to provide a
factory, I promise", we'd need "code generation knobs." And once you
add one such knob, you will invariably have a dozen, and now, rather
than being a language feature with defined semantics, you've stuck a
crappy macro generator in your language where no one can rely on the
resulting semantics.
In (4), you say "assignment without <this> is unnatural." With respect,
this is just saying "Augh, the learning, it hurts!" Similarly, "looks
nothing like Java" could be leveled against NEARLY EVERY feature we've
added. On day 1, it looks unnatural, because its new, but then we
learn. In Java 5, people said "Augh, List<String> looks like C++." In
Java 8, they said "x -> x + 1 looks nothing like Java." In Java 10,
they said "var x = e" looks like Javascript. But, how uncomfortable are
you with these locutions today?
I think too you've misunderstood what is going on. You say:
> public Example4 {
> x = x + 1; // assigns field, but looks nothing <like Java>
> }
but this is not reassigning the field! This is merely transforming the
parameters. The parameers are then committed, in bulk, to the fields at
the end. So the compact constructor effects a (usually trivial)
transform on the record state, _and then_ stamps that into a new record.
I think when you learn how this works, and get used to it, you'll
probably like it better. It surely has made much of the boilerplate go
away, leaving the code that actually matters in the foreground.
> this.x = x + 1; // read it might be forbidden in the future
already forbidden. Having two ways of doing it was confusing, as your
note demonstrates.
In your (5), you want to expose a constructor that adds an offset to X.
Kind of a bad semantic fit for a record, but trivially expressed:
record Ex5(int x) {
// implicit canonical ctor
Ex5(int x, int plus) { this(x + plus); }
}
In all of these, you are getting caught up on: "but I must initialize
the fields via assignment." Let go of that, and the answer becomes
obvious, and your code gets better. Note that in all the examples
above, no one had to write a field assignment.
tl;dr: I think this is just the growing pain of "this is different, and
different is bad." But if you stop thinking of records as "more concise
classes", and instead as "nominal tuple objects", this gets a lot easier
-- and you'll probably like it.
Perhaps a better set of questions is: "will we ever get these goodies
for regular classes too?" We're working on it.
Cheers,
-Brian
>
> The following examples are a bit constructed and don't make logical
> sense,
> but I hope my idea gets across :)
>
>
> 1. Canonical constructor can not call <custom constructor>, e.g.
> delegating to spezialized normalizing constructor with a constant default
> value:
>
>
> record Example1(int x) {
>
> public Example1(int x) {
>
> this(x, 0); // compile error
>
> }
>
>
> public Example1(int x, int defaultX) {
>
> this.x = x >= 0 ? x : Math.max(defaultX, 0);
>
> }
>
> }
>
>
> 2. <Custom constructors> must call canonical constructor, e.g. support
> multiple input types or when you can't reuse canonical constructor:
>
>
> record Example2(UUID uuid) {
>
> public Example2 {
>
> uuid = UUID.nameUUIDFromBytes(uuid.toString().getBytes());
>
> }
>
>
> public Example2() { // compile error
>
> this.uuid = UUID.randomUUID();
>
> }
>
>
> public Example2(String uuid) { // compile error
>
> this.uuid = UUID.fromString(uuid);
>
> }
>
> }
>
>
>
> 3. Canonical Constructor must be public, so it's not possible to have
> only static factories or it forces normalization outside of Records
> constructor:
>
>
> record Example3(UUID uuid) {
>
>
> private Example3 { // compile error
>
> }
>
>
> public Example3(String uuid) { // compile error
>
> String uuidNormalized = StringUtils.toLowerCase(uuid);
>
> try {
>
> this.uuid = UUID.fromString(uuidNormalized);
>
> catch(IllegalArgumentException e) {
>
> this.uuid = null;
>
> }
>
> }
>
>
> public static Example3 fromString(String uuid) {
>
> return new Example3(uuid);
>
> }
>
> }
>
>
> 4. Assignment without <this> in canonical constructor is very
> unnatural, e.g. use of <this> in Classes is very logical when you would
> otherwise reassign the parameter
>
> In Records however :
>
>
> record Example4(int x) {
>
> public Example4 {
>
> x = x + 1; // assigns field, but looks nothing <like Java>
>
> }
>
>
> public Example4(int x, int plus) {
>
> this(x); // hrrng
>
> x = x + plus; // reassign parameter
>
> }
>
> }
>
>
> record Example5(int x) {
>
> public Example5 {
>
> this.x = x + 1; // read it might be forbidden in the future :(
>
> }
>
>
> public Example5(int x, int plus) {
>
> this(x); // hrrng
>
> this.x = x + plus; // is compile error
>
> }
>
> }
>
>
> 5. It's not possible to define the canonical constructor with the exact
> same parameter list as the Record definition has.
>
> The simple canonical constructor without parameters should just be an
> alias
> for that in my opinion.
>
> For symmetrie reasons (with other constructors) and clearity I would
> like to
> define the canonical constructor with the required parameters.
>
>
> I think most of those discrepancies (especially 4) and 5)) come from the
> desire to be less verbose and the definition of records (state and only
> state) and still have some <convenience> like Classes.
>
> However the limitations and half-way implicit <magic> makes it really
> confusing. I think it would have been better to keep constructors
> similar to
> the ones from Classes if explicitly defined.
>
> I mean if the user is already going to manually write constructors (which
> should be a special case) the few lines of assignments don't matter
> compared
> to the rethinking it needs each time
>
> with implicits and the restrictions that follow.
>
>
> What are your thoughts?
>
>
> Kind regards,
>
>
> Simon
>
>
>
>
More information about the amber-dev
mailing list