Data Oriented Programming, Beyond Records
Brian Goetz
brian.goetz at oracle.com
Wed Jan 21 19:34:17 UTC 2026
> In other words, I like Remi's conceptual idea of carrier classes being
> "(possibly abstract) things with virtual components". Really that's
> almost like saying "modernized java beans". Then a record could still
> be a specialized, locked down, concrete form of a carrier class.
It is fair to say "I want to consider a different semantic meaning for '
a class that has a state description '." And it is also fair to
question "did records get it wrong?" as part of that discussion. Indeed,
there were half a dozen different potential semantics for "class with
header" that were raised during the design of records, and this variety
was one of the reasons its taken us this long to figure out which we
like the best.
One of the implicit, but deeply held, design choices made here is that
the class header is a semantic statement about what this class *is*.
Records had the cutesy slogan "the state, the whole state, and nothing
but the state", which may or may not be the best slogan, but it does
allow you to describe record-ness pretty concisely. We are looking to
get as close to such a crisp semantic statement as possible, ideally
leaving words like "generate" out of the process.
The proposal we put forth is that a carrier class C for state
description S *is* a carrier for the data tuple described by S, and
anything more is "incidental". This turns out to be a pretty strong
statement:
- If C is just a carrier for an S-shaped slug of data, then (for
concrete C) I must be able to take an S-shaped slug of data and produce
a C (canonical constructor.)
- If C is just a carrier for an S-shaped slug of data, then I must be
able to extract that slug from a C (deconstruction pattern.)
- If C is just a carrier for an S-shaped slug of data, then taking a C
apart (into an S), and creating a new C from that same S (via the
constructor) _must yield an equivalent C_. Otherwise is is not "just" a
carrier for S.
If you read the spec for Record::equals, you'll see that this invariant
-- take it apart with accessors, put it back together unchanged with the
constructor, the result must be equals() -- is written into the Record
spec. Not because we chose to "generate" equals and hashCode, but
because of what the "just" in "just a carrier for S" means.
If this were not the case for carriers, then it must be because carriers
are making some much weaker claim about what carrier-ness means. But
you can't claim much less without giving up a lot of benefits, too. If
we backpedal on the _completeness_ claim (the state description is a
complete, canonical, nominal description of the classes state), you
could argue that we then have no basis for default equality semantics
other than identity-based, but we also undermine the semantic basis for
reconstruction -- without the completeness claim, then `x with {}` can't
be seen as "give me an equivalent x".
This model of "a carrier class is one that has at these properties" is a
credible one (in a vacuum), but you get much less for it; you don't get
reconstruction, the constructor and deconstructor can't really be
considered canonical, and it is a semantic departure from recordness.
More information about the amber-spec-experts
mailing list