Feedback on reconstruction/'withers': Lombok lessons.

Brian Goetz brian.goetz at oracle.com
Sat Aug 15 20:56:13 UTC 2020


Thanks for the actually-grounded-in-experience feedback!

I totally believe that withers have been unpopular within the Lombok 
community, but I think there may also be a degree of self-selection 
there too.  And yes, if you're plunking for builders, then a 
relative-builder is cheap, and inline classes can reduce the runtime costs.

But, the language has degrees of flexibility that Lombok does not.  
Lombok is about generating code that you could write yourself but don't 
want to (and don't want to read it).  That has a role, and certainly 
tools like Lombok can move faster than the language, and the community 
can support multiple Lombok-like tools without problem.  But the 
language can bypass generated code by imbuing new constructs with 
semantics, and this has significant potential.  For example, records are 
not just immutable POJOs with generated members; there is a semantic 
guarantee (which of course people can break if they are careless) that 
gives frameworks permission, say, to transparently serialize records to 
and from JSON without any user hints.

Many of the patterns that Lombok automates away (getters and setters, 
builders, etc) could be the subject of the old barb "design patterns are 
indications of language failings" (usually said with a great deal of 
self-satisfaction, accompanied by trying to sell something.)  Builders 
are in that category; many builders could go away with a decent facility 
for optional constructor parameters.  (It's no accident that we used 
__byname as our concept placeholder.)

I think, too, that the popular patterns in Lombok are backward looking, 
but the language is making a strong move (records, inline classes) in 
the direction of more immutability.  SO I expect usages to equilibrate 
in a different place in a few years, and we're trying to get ahead of 
that curve.

One point that nearly everyone has missed about this idea; the stuff in 
the braces is not necessarily just a "named argument list"; it's an 
_arbitrary block of Java code_ (which has something in common with the 
compact constructor of a record, in that the values of the synthetic 
locals at the end of the block are committed to the object state.)  It 
can have loops, conditionals, etc.  Imagine a lambda could take in N 
parameters and yield new versions of those N parameters; that's a better 
model for what the block does.  It's not just about automating builders.

Your comments about template values are well taken; we've explored some 
of these ideas too.

> minorityReport.withDirector(minorityReport.getDirector().withDateOfBirth(minorityReport.getDirector().getDateOfBirth().withYear(1956)));
>
> Oof. I don't immediately see anything in the strawman `with` syntaxes that
> would make this any better.

Yes, the problem of deep nesting is one we are well aware of. The point 
of this writeup was not the syntax, but the concept of functional 
transformation in an imperative language -- and how we can derive such a 
mechanism from what we already have (or almost have.)  Remi's example is 
the obvious transformation but we can probably do better.






On 8/14/2020 7:51 PM, Reinier Zwitserloot wrote:
> In response to:
> https://github.com/openjdk/amber-docs/blob/master/eg-drafts/reconstruction-records-and-classes.md
>
> Lombok has had withers for a decade. Almost nobody uses them. However, take
> that with a grain of salt; wither has been in our experimental package for
> most of that time, and a significant chunk of lombok users won't use
> features from that package, vs. its closest competitors (@Builder, and
> having a mutable type instead) which have been in the main package for
> (almost) their entire lifetime. Then again, we got very few requests to
> upgrade Wither to core support status (we did upgrade it last year, but
> mostly because we wanted to, and not due to much community demand; other
> experimental features get far more requests to get upgraded to core support
> levels).
>
> Contrast to `@Data` (makes setters and getters, i.e. solve the problem by
> making your type mutable, which is not a direction that records want to go
> to, obviously), and it's no contest: @Data is many orders of magnitude more
> popular.
>
> Even `@Builder` is an order of magnitude or two more 'popular' than @Wither.
>
> I like withers. However, You can both get rid of them, and solve the
> combinatorial explosion problem, if instead you allow records to
> automatically make you a builder, _and_ if you can turn an existing record
> instance into a builder too, that acts a lot like withers do (looking ahead
> to valhalla, if we have a valhalla codes-like-an-class-performs-like-an-int
> class, is 'instance' still the right word? I digress).
>
> Consider a Bridge class that has a few properties, including 'name',
> 'buildYear', and 'length':
>
>      Bridge goldenGate = Bridge.builder()
>          .name("Golden Gate")
>          .buildYear(1937)
>          .length(8980)
>          .build();
>
> Assuming Bridge is immutable here, I need to correct my mistake; the length
> should obviously be in meters, not feet. Lombok offers two different
> options:
>
> * annotate the `length` field with a `@With` annotation (we very recently
> renamed it from `@Wither` to `@With`), then:
>
> goldenGate = goldenGate.withLength(2737);
>
> * allow builder to also build off of instances ( via @Builder(toBuilder =
> true)):
>
> goldenGate = goldenGate.toBuilder().length(2737).build();
>
> contrasting these two options:
>
> * the toBuilder route is a lot of syntax especially if you want to modify
> only one property.
> * toBuilder always makes exactly 2 objects (the builder, and the new
> instance), regardless of how many properties you modify. the with methods
> will create 1 object per with invocation. If you update 4 properties, you
> get 3 intermediate garbage objects, and one final one you actually wanted.
>
> I can see ways of eliminating the builder-route's intermediate object. I'm
> not sure it's particularly performance relevant for the base records case,
> but once you toss valhalla into the mix, that builder object is probably
> not palatable.
>
> builders have the exact same* memory load as the base classes do: One for
> each field; one could imagine a system where a builder is turned into the
> object it is trying to build simply by overwriting those parts in heap
> memory that represent 'what type am I', leaving the actual field data
> unmolested. With builders, the problem is, someone may still hold a
> reference to the builder which would violate heap rules, but with syntax
> sugar, you can ensure that no code could possibly have a ref to the builder
> (which has now magically turned into the object it was building). This
> concept could then extend to valhalla types in time (where, presumably, the
> "fields" are all on-stack).
>
> That just leaves the syntax which is a bit unwieldy. That too seems
> fixable, this time with some sugar: (<insert usual Brian strawman syntax
> disclaimer here>):
>
>      Bridge goldenGate = Bridge.build {
>          name = "Golden Gate",
>          buildYear = 1937,
>          length = 8980,
>      };
>
> (I would strongly advise that trailing commas are allowed, just like they
> are in array decls and enum decls).
>
> and 'build from instance' could then become:
>
>      Bridge fixedGoldenGate = goldenGate.with { length = 2737 };
>
> all that being syntax sugar (as in, there still is a builder() method, and
> it has a build() method - the above is just syntax sugar to invoke
> them, although with the sugar you may get the benefit of getting the
> builder instance optimized out). This also gives the benefit that if you
> want to make a builder, fill in half of the fields, then pass the builder
> itself off to a helper method, you can do that (just don't use the syntax
> sugar), whereas with the with{} concept that becomes a little harder,
> perhaps.
>
>   --Reinier Zwitserloot



More information about the amber-dev mailing list