There is a whole category if "ooh, shiny new language construct, this is
an opportunity to slide in some new features." We saw this with lambdas
-- "can you please make the parameters of lambdas implicitly final."
The motivation for such requests is obvious; we wish all parameters
could be implicitly final (perhaps with an opt-out), but we can't turn
back the clock, so we console ourselves with "well, at least I can have
_some_ finality.) But this consolation is often disappointing, because
it creates (a) challenges for migration between existing constructs
(anon classes) and the new construct (lambdas), often in both
directions, and (b) requires users to keep track of complex,
history-driven rules for when they can use a feature ("oops, can't have
static fields in inner classes.") These are often a siren song, and are
best avoided.
Turning to records, an example of a feature we've rejected on the basis
of this siren song: Named parameter invocation (`new Point(x: 1, y:
2)`.) Yes, parameter names are part of the API, so this becomes easier
for records than constructors/statics (which is easier, in turn, than
for instance methods.) Instead, we specified that record component
names are always reified through reflection, so that if/when we ever get
to a more general story for named invocation, records will be ready.
While the user burden is less in this case, compiling `new R(...)` to a
factory invocation feels like pushing on the wrong end of the
"constructor rehabilitation" lever, because doing so will create binary
compatibility trouble for records that want to migrate back to classes.
(Like with enums, this is not 100% compatible, because of the supertype
(Enum/Record), but like with enums, it is not uncommon to hit the wall
and want to refactor back to classes. Let's not make this harder.)
So my vote is #0. But, if we have a story for moving _all_ classes
towards factory construction, then we should make sure records play
well. I don't think we have to do anything now for which we'd regret
inaction, because whatever problem we have with legacy records, we'll
already have with every class ever written.
On 1/10/2020 1:36 AM, John Rose wrote:
Another bit of translation strategy: What does “new R(a,b,c)” compile
to, at the bytecode level? At some point we will cut over to factory methods,
for at least some types (inlines). Should be pre-empt this trend for records,
and mandate that records are *always* created by factory methods?
The benefit is that records can be evolved to inline types without
recompiling their *clients*. The risk is that (if we don’t take this
option) that we will have records which are constrained to be identity]
types *until all clients are recompiled*. (No I don’t believe that you
can translate “new;dup;invokespecial<init>” into “invokestatic”.
Sorry, that's a pipe dream.) This seems sad, given that records are
partially inspired by value types.
(From my 2012 blog on value types: “A value type is immutable… The
equals and hashCode operations are strictly component-wise.” Kind
of like a record. Meanwhile, inlines in Valhalla give component-wise
meaning to acmp not equals and identityHashCode not hashCode.)
I see three options regarding factories (apart from the above pipe dream):
0. Do nothing. Records are just abbreviated classes. Whatever works
or doesn’t for evolving general identity classes to inline classes works
or doesn’t for records. No special benefit, in this vein, to declaring
something a record.
1. Define a factory, on top of today’s JVM. Records use a hidden API,
desugaring “new R(a,b)” to a static “R.$make$(a,b)”.
2. Define a factory, anticipating Valhalla, using a JVM-defined entry point.
The expression “new R(a,b)” compiles to “invokestatic R.<init>(int,int)R”,
preceded by a push of a and b. This requires a (relatively shallow) change
to the JVM to double down on the name “<init>” as the factory behind a
constructor.
The advantage of 2 is that, if we correctly predict that “<init>” is the claimed
factory method for value types, then records can be evolved to inlines out
of the box. The advantage of 1 is that we don’t need to make that prediction.
There’s also this:
3. Do 1. but when 2. becomes an option create an auto-bridge from “$make$”
to “<init>”. New code doesn’t need the bridge because it compiles to call
“<init>” out of the box.
Here’s a gut check for this group: Are we confident enough with Valhalla
that we can settled on “invokestatic <init>” as the new dance for creating
an instance that may or may not be an inline?
— John