Feedback on reconstruction/'withers': Lombok lessons.
Reinier Zwitserloot
reinier at zwitserloot.com
Fri Aug 14 23:51:25 UTC 2020
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