Feedback on records (JEP 359)
Brian Goetz
brian.goetz at oracle.com
Sat Apr 25 15:42:25 UTC 2020
> 1) a request: besides Brian's datum write-up [1], it would be useful
> to have a more practice-oriented document, similar to the one for text
> blocks [2].
Yes, we plan to do this for all the features. I think what you're
really asking is: "is it possible to have this at the time the feature
previews, so that more people can get started on the right foot, and to
guide feedback?" It is certainly possible to juggle things like this,
of course, what it means is that it's more work to deliver a feature,
which means it will take longer to deliver features. We had been
planning to do these docs in the shadow of the release.
> For example, one thing I'd like such document to expand on, would be
> migration compatibility between records and classes (i.e. if I expose
> a record in my public API, how will that impact my ability for
> evolving it, e.g. converting it into a class?). This would ideally
> also include a FAQ which consolidates the wisdom from the mailing
> lists (e.g. "Why is the canonical constructor specified to be public?"
> [3], "Why aren't accessor methods generated as final?" [4], etc.).
We did a FAQ for `var`. The danger in doing FAQs too early, of course,
is that you are guessing at what the frequently asked questions will be,
rather than actually observing what they are.
It would be a great thing for the community to gather these kinds of
questions for possible FAQ entries! We're usually focused on the next
thing, so sometimes they recede into the sands of time.
> 2) a proposal: a method `Stream<IndexedElement<T>> indexed()` on
> Stream, which wraps each Stream element in a `record
> IndexedElement<T>(long index, T t)`
> This would allow for a natural, concise way to access indices (cf.
> e.g. [5]).
This one was explored during Lambda. There were two concerns with it:
- Doing so makes an awful lot of garbage, and users may be surprised
by this. (When we get to Valhalla, this concern will go away.)
- For sources that are not ideally splittable (where splitting yields
not only approximate balance, but precise left/right counts), parallel
operations become effectively sequential, since you can't start the
right half of the computation until the left half finishes and you know
where to start the right count. Again, this may be surprising to users.
I suspect that when we clear the former objection, the bar will be low
enough to proceed on this. (FWIW, has all the same problems as zip,
which was explored and postponed for the same reasons.)
> it reminds people that the canonical constructor is:
> a) present (unlike classes, where, if you define a constructor
> yourself, the implicit constructor doesn't exist)
> b) public (if there's a single private constructor, I regularly forget
> that there's still a public constructor as well)
I realize there is a precedent that "you only get a free constructor if
you don't bring one of your own." But, do you believe that Java
developers are unable to learn a refined version of this rule for records?
We didn't need records at all, of course; we did them to lower the
barrier to using properly abstracted data aggregates. Every bit where
we force people back into more ceremony takes away from that benefit. I
would worry the cost/benefit here is not in the user's favor.
> 5) an observation/proposal: I need to remind myself sometimes that
> record components are nullable. Would it be an option to introduce a
> `nullable-record` keyword, where `record` would not allow any of its
> components to be null, whereas `nullable-record` would?
There is a whole list of "goodies" people have asked for in records,
that they really want for all classes, but are tired of waiting so hope
that they could sneak into the back of the records bus. This is one of
them :)
It is attractive to try and staple general features onto records, but I
think in the end this is counterproductive. It increases the complexity
of the language ("with records, you can do X and Y and Z that have
nothing to do with records but are cool, but you can't do them with
classes"), which is more stuff for users to keep track of, but worse, it
means that if you exceed the bounds of what records do, you can't
necessarily refactor back to a class.
If we did this (or another common one in this bucket, constructor
invocation by parameter names instead of position), then if you tried to
refactor to a class, your clients would be hosed -- because their client
code would not work against a class. (In the named invocation case, the
client would fail compilation; in the "non null" case, it would compile
but the semantics would be subtly changed, and any code that assumed
"this can't possibly be null", which might have been a reasonable
assumption at the time the code was written, is now vulnerable.)
It is better to address these as general features, even if it means it
will take longer to deliver.
> 6) a proposal: there's a recurring occurrence of boilerplate in my
> records, which could be eliminated by introducing a `sorted-record`
> keyword. This would work as follows:
>
> sorted-record YearMonth(Year year, Month month) {}
>
> is equivalent to:
>
> record YearMonth(Year year, Month month) implements
> Comparable<YearMonth> {
>
> private static final Comparator<YearMonth> NATURAL_ORDER = Comparator
> .comparing(YearMonth::year,
> Comparator.nullsLast(Comparator.naturalOrder()))
> .comparing(YearMonth::month,
> Comparator.nullsLast(Comparator.naturalOrder()));
>
> @Override
> public int compareTo(YearMonth o) {
> return NATURAL_ORDER.compare(this, o);
> }
>
> }
We considered which additional protocols (aside from construction,
deconstruction, equality, hashing, and string display) could and should
be derived from the state description, and Comparable was the first one
on that list. The problem with doing this is that, unlike the ones
listed above, the order in which the components are compared is
semantically significant, *and*, when you want comparators for records,
you often don't even want to include all the components. The
"automatic" comparison protocol you'd derive from the state description
would almost never be what you wanted, and then you'd be coming back
asking for "knobs" to determine which components flow into the
comparison and in what order.
FWIW, most of the actual boilerplate in your above Comparator is folding
in the null handling. It might be an RFE on Comparator factories would
be more effective in solving this problem, and also more useful (as it
wouldn't only apply to records.) If your comparator was (say)
Comparator.comparingNullable(YearMonth::year)
.thenComparingNullable(YearMonth::month);
or similar, would you be as concerned?
One more point on this: when we did streams, we started down the
direction of having sort methods on streams, and it kind of exploded; we
wanted to sort not only by natural-ordered comparables, but with
comparators, and with primitives, which meant for five variants; reverse
sort was another 2x; the four stream implementations was another 4x, so
that meant 60 sorting methods just in streams, all motivated by wanting
to be able to sort by providing a lambda that extracted a sort key.
Then we realized we were being idiots, that adding half a dozen
factories and combinators for Comparator cut through the explosion, but
also, that they were useful far beyond streams. I think the same thing
is true here; the problem your are experiencing comes via records, but
it is really a problem with Comparator, and it is better to address the
problem at the root.
> This is tedious to write, but seems relatively easy for a compiler to
> generate.
... but only if the compiler knows which fields are significant for
comparison, and in which order you want to compare them.
More information about the amber-dev
mailing list