Draft JEP: Derived Record Creation (Preview)
Kevin Bourrillion
kevinb9n at gmail.com
Fri Mar 1 19:09:08 UTC 2024
Hi Gavin,
My response is mostly just to add grist to the mill for a feature that
looks great already. You might perhaps feel some of the points are worth
working into the proposal.
I have some angst over the term "derived" for this. It's not wrong, but is
an entirely different meaning from the one I encounter regularly: a
"derived field" being one that caches a value computed deterministically
from the other field values (a feature that record classes notably don't
support... sadly).
I think the more basic term is "modified", and I think it works: "creating
modified records". In the vernacular I think most people do understand that
a genetically "modified" soybean doesn't necessarily mean that any
particular bean was changed, only that it is an altered version of what it
would otherwise have been. "You can't *modify* a record instance, but you
can get a *modified* instance based on it." This feels to me like a good
reuse of existing terminology.
Suppose we want to evolve the state by doubling the x coordinate of a Point
> oldLoc, resulting in Point newLoc:
>
> Point newLoc = new Point(oldLoc.x()*2, oldLoc.y(), oldLoc.z());
> This code, while straightforward, is laborious. Deriving newLoc from
> oldLoc means extracting every component of oldLoc, whether it changes or
> not, and providing a value for every component of newLoc, even if unchanged
> from oldLoc. It would be a constant tax on productivity if developers had
> to repeatedly deconstruct one record value (extract all its components) in
> order to instantiate a new record value with mostly the same components.
>
It's also bug-prone in multiple ways.
It also is the worst-case maintenance scenario, the "any-every". When
adding, removing, or renaming *any* record component, *every* statement
like this throughout the codebase has to be changed. (True of record
constructor calls too, but there's not much we could do about that short of
optional parameters.)
However, wither methods have two problems. First, they add boilerplate to
> the record class,
>
Some boilerplate is relatively innocuous but this is
*high-maintenance* boilerplate,
which we have to carefully keep in sync with the record's component
declarations. Boo.
> Record values can be nested, where components are themselves record
> values. Derived instance creation expressions can be nested in order to
> transform nested record values. For example:
>
> record Marker(Point loc, String label, Icon icon) { }
>
> Marker m = new Marker(new Point(...), ..., ...);
> Marker scaled = m with { loc = loc with { x *= 2; y *= 2; z *= 2; }};
>
In fact, this is such a common need (in my experience), and what you have
to do today is such a horror show, that you might want to illustrate it as
part of the value proposition of the feature.
> Derived instance creation expressions can be used in record classes to
> simplify the implementation of basic operations. For example:
>
> record Complex(double re, double im) {
> Complex conjugate() { return this with { im = -im; }; }
> Complex realOnly() { return this with { im = 0; }; }
> Complex imOnly() { return this with { re = 0; }; }
> }
>
This is very nice because now `conjugate()` has no relationship with `re`
at all, just as it should be. And it makes the essence of what each method
is for crystal-clear.
> Any assignment statements that occur within the transformation block have
> the following constraint: If the left-hand side of the assignment is an
> unqualified name, that name must be either (i) the name of a local
> component variable, or (ii) the name of a local variable that is declared
> explicitly in the transformation block.
>
And because there's no way to qualify a local variable from the surrounding
scope, reassigning such variables is simply impossible within this block.
Right?
That's "no great loss" of course, although I'm missing why the restriction
is necessary. The notion of variables that (at least in userspeak) are "in
scope for reading but not for writing" seems weird; does it have precedent?
The transformation block need only express the parts of the state being
> modified. If the transformation block is empty then the result of the
> derived instance creation expression is a copy of the value of the origin
> expression (the expression on the left-hand side).
>
This could be interpreted as saying that in this case the record's
constructor isn't even run, which I suspect isn't what you mean, and which
could make a difference (if best practices aren't being followed). Do you
need to say anything at all about this case?
If the origin value is null then evaluation of the derived instance
> creation expression completes abruptly with a NullPointerException.
>
... and if it isn't, then we can talk about the "origin instance" it refers
to.
I'd suggest avoiding the term "origin value" completely except for the
above, preferring to talk about the origin instance instead. I think that's
the way to be as clear as possible that none of what we're talking about
here cares whether the record class is a value class or not. But we can
dissect this further if need be.
Before executing the contents of the transformation block, a number of
> implicit local variable declaration statements are executed. These local
> variable declaration statements are derived from each record component in
> the header of the record class R, in order, as follows:
>
> The local variable declaration has the same name and declared type as the
> record component.
>
Overall, there have been several references here to the record class R, but
I would think it's the record *type* we really need to talk about. That
type post-substitution is what determines these variable types, no?
That also suggests we need to discuss wildcard capture here - or is that
addressed elsewhere?
A new instance of record class R is created as if by evaluating a new class
> instance creation expression (new) with the compile-time type of the origin
> expression and an argument list containing the local component variables,
> if any, in the order that they appear in the header of record class R.
>
Likewise, should this talk about type arguments too? This would I think
mean duplicating them from the record type but doing whatever fancy
footwork is required to deal with wildcards? (I assume that record
constructors themselves can't be generic.)
Implied in all this: I would think a record type like `MyRecord<String, ?>`
*should* be usable with `with` (of course, trying to assign to some
variables inside the transformation block isn't going to go well, but
likely the user just isn't referring to those variables at all in this
case).
> The use of a derived instance creation expression:
> can be thought of a switch expression:
>
imho this would be useful to state earlier!
What goes wrong if we think of this feature as *exactly* desugaring to that
switch code?
The structure and behavior of the transformation block in a derived
> instance creation expression is similar to the body of a compact
> constructor in a record class. Both have the same control flow restrictions
> (must complete normally or throw an exception); both have a set of
> pre-initialized variables in scope, which are expected to be mutated by the
> block; and both take the final values of those variables and pass them as
> arguments to a constructor invocation.
>
This would've been useful to state earlier too, to me anyway. The only
difference I thought of is that one can refer to `this` inside the
constructor (uh, right?) but there is no syntax to access the origin
expression in the transformation block. And that seems as it should be.
Alternatives
> Instead of supporting an expression form for use-site creation of new
> record values, we could support it at the declaration site with some form
> of special support for wither methods. We prefer the flexibility of
> use-site creation, whereas declaring wither methods would add bloat to
> record class declarations, which currently enjoy a high degree of
> succinctness.
>
This would also introduce a lot of potential for unpredictability. The
whole deal with records is that they act in highly predictable ways.
~~
Nano-scale details aside... this will be a very helpful feature for working
with records and I hope it happens!
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-spec-experts/attachments/20240301/31c1f330/attachment.htm>
More information about the amber-spec-experts
mailing list