"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