"With" for records
Brian Goetz
brian.goetz at oracle.com
Sat Jun 11 14:25:20 UTC 2022
I got a private mail asking, basically: why not "just" generate withers
for each component, so you could say `point.x(newX).y(newY)`.
This would be a much weaker feature than is being proposed, in several
dimensions.
1. It doesn't scale to arbitrary classes; it's a record-specific hack.
Which means value classes are left out of the cold, as are immutable
classes that can't be records or values for whatever reason. The link I
cited suggests how we're going to get to arbitrary classes; I wouldn't
support this feature if we couldn't get there.
2. It is strictly less powerful. Say you have a record with an
invariant that constraints multiple fields, such as:
record OddOrEvenPair(int a, int b) {
OddOrEvenPair {
if (a % 2 != b % 2)
throw new IllegalArgumentException();
}
}
This requires that a and b both be even, or both be odd. Note that
there's no path from (2, 2) to (3, 3); any attempt to do `new OOEP(2,
2).a(3).b(3)` will fail when we try to reconstruct the intermediate
state. You need a wither that does both a and b at once. (And we're
not going to generate the 2^n combinations.)
On 6/10/2022 8:44 AM, Brian Goetz wrote:
> In
>
> https://github.com/openjdk/amber-docs/blob/master/eg-drafts/reconstruction-records-and-classes.md
>
> we explore a generalized mechanism for `with` expressions, such as:
>
> Point shadowPos = shape.position() with { x = 0 }
>
> The document evaluates a general mechanism involving matched pairs of
> constructors (or factories) and deconstruction patterns, which is
> still several steps out, but we can take this step now with records,
> because records have all the characteristics (significant names,
> canonical ctor and dtor) that are needed. The main reason we might
> wait is if there are uncertainties in the broader target.
>
> Our C# friends have already gone here, in a way that fits into C#,
> using properties (which makes sense, as their language is built on that):
>
> object with { property-assignments }
>
> The C# interpretation is that the RHS of this expression is a sort of
> "DSL", which permits property assignment but nothing else. This is
> cute, but I think we can do better.
>
> In our version, the RHS is an arbitrary block of Java code; you can
> use loops, assignments, exceptions, etc. The only thing that makes it
> "special" is that that the components of the operand are lifted into
> mutable locals on the RHS. So inside the RHS when the operand is a
> Point, there are fresh mutable locals `x` and `y` which are
> initialized with the X and Y values of the operand. Their values are
> committed at the end of the block using the canonical constructor.
>
> This should remind people of the *compact constructor* in a record;
> the body is allowed to freely mutate the special variables (who also
> don't have obvious declarations), and their terminal values determine
> the state of the record.
>
> Just as we were able to do record patterns without having full-blown
> deconstructors, we can do with expressions on records as well, because
> (a) we still have a canonical ctor, (b) we have accessors, and (c) we
> know the names of the components.
>
> Obviously when we get value types, we'll want classes to be able to
> expose (or not) such a mechanism (both for internal or external use).
>
> #### Digression: builders
>
> As a bonus, I think `with` offers us a better path to getting rid of
> builders than the (problematic) one everyone asks for (default values
> on constructor parameters.) Consider the case of a record with many
> components, all of which are optional:
>
> record Config(int a,
> int b,
> int c,
> ...
> int z) {
> }
>
> Obviously, no one wants to call the canonical constructor with 26
> values. The standard workaround is a builder, but that's a lot of
> ceremony. The `with` mechanism gives us a way out:
>
> record Config(int a,
> int b,
> int c,
> ...
> int z) {
>
> private Config() {
> this(0, 0, 0, ... 0);
> }
>
> public static Config BUILDER = new Config();
> }
>
> Now we can just say
>
> Config c = Config.BUILDER with { c = 3; q = 45; }
>
> The constant isn't even necessary; we can just open up the
> constructor. And if there are some required args, the constructor can
> expose them too. Suppose a and b are required, but c..z are
> optional. Then:
>
> record Config(int a,
> int b,
> int c,
> ...
> int z) {
>
> public Config(int a, int b) {
> this(a, b, 0, ... 0);
> }
> }
>
> Config c = new Config(1, 2) with { c = 3; q = 45; }
>
> In this way, the record acts as its own builder.
>
> (As an added bonus, the default values do not suffer from the "brittle
> constant" problem that a default value would likely suffer from,
> because they are an implementation detail of the constructor, not an
> exposed part of the API.)
>
>
> I think it is reasonable at this point to take this idea off the shelf
> and work towards delivering this for records, while we're building out
> the machinery needed to deliver this for general classes. It has no
> remaining dependencies and is immediately useful for records.
>
> (As usual, please hold comments on small details until everyone has
> had a chance to comment on the general direction.)
>
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20220611/7cc8a9a7/attachment.htm>
More information about the amber-spec-experts
mailing list