Getters vs. public final fields in records
Brian Goetz
brian.goetz at oracle.com
Wed Aug 10 17:32:59 UTC 2022
So, I want to make sure this conversation stays on track. You asked
"why does it work this way", and you got an answer. And its OK to ask
follow-up questions for clarification. But we need to steer away from
the (irresistibly tempting, I know) track of "let's redesign the
feature", and we're in danger of veering into that. These issues were
well considered during the design already, and these questions are not
new. As mentioned, we did explicitly consider public fields, and
concluded that would be a bad idea. (I know this may sound unfriendly,
but just consider how this scales. There are 10M Java developers, who
may each independently have their own ideas about how a feature should
be designed, and want to "debate" it, sequentially and independently and
possibly inconsistently, with the language designers.)
Now, to your question.
> If we look at what record-like things look like in other languages and
> settings (C structs, ML records, Pascal records) accessors are the
> strange choice that needs justification.
>
> In any case, the answer is simple: not all objects are immutable.
> If a
> record component were an array, or an ArrayList, or any other object
> with mutable state, having public fields would make it impractical to
> use these types in records in many cases, because we'd be unable to
> expose their state without also exposing their mutability.
>
>
> I think you are suggesting techniques like explicitly defining
> accessors for mutable record components that make defensive copies?
> Are we really comfortable calling records “a simple aggregation of
> values” if I have to read the documentation or implementation of an
> accessor just to understand what the following code does:
>
This is exactly as we intended it to work. For records whose components
are values (e.g., `record Point(int x, int y) {}`), the default
implementation of constructor and accessor do exactly the right thing,
and no one has to write any code. But when mutability rears its head,
the "right thing" is less obvious, and making a one-size-doesn't-fit-all
decision will make some users unhappy.
As it turns out, the mathematical construct from Domain Theory known as
"embedding-projection pairs" (which is a formalization of approximation)
offer us a useful way to talk about the right thing. The key invariant
that records must adhere to (specified in the refined contract of
`Record::equals`) is that unpacking a record components, and repacking
in a new record, should yield an "equals" record:
r.equals(new R(r.c0(), r.c1(), ..., r.cn())
(This is as close as we can say in Java to "construction and
deconstruction form an embedding-projection pair between the space of
records and the cartesian product space of their components.")
The reason that the situation you describe is inevitable comes from the
fact that for mutable components (such as arrays), in some cases we want
to judge two records equal if they hold the same _array object_, and in
other cases we want to judge two records equal if they hold arrays _with
the same contents_. And the language doesn't know which you want -- and
it requires very little imagination to construct cases where each of
these interpretations will be wrong. So unless we're willing to outlaw
records that are not immutable all the way down, which would make
records much less useful, we have to give people a way to say what they
want. This is just like how we let map classes decide whether equals()
means "same map", or means "same mappings". Both are valid
interpretations, and both should be expressible. Similarly, some
records may want to expose the mutability of their components to
clients, and others will want to launder using defensive copies. All of
these are expressible in the record model, just with different degrees
of explicit code.
If your component already chooses the answer for equals that you want --
such as ArrayList::equals comparing lists by contents, or arrays
comparing by identity -- then you can do nothing. Otherwise, you have
to override the constructor, accessor, and equals in concert to preserve
the invariant that deconstruction and reconstruction yields something
equivalent to the original.
So if you define a record whose components are mutable, or for whom you
don't want the record equivalence semantics to be the same as the
component equals, you're going to have to write some code -- but more
importantly, you're going to have to tell your users what equality means
for *your* record. Just like you're supposed to specify what equality
means for every class. It might be tempting to say "records are just
structural tuples, so there's nothing interesting to say about
equality", but that turns out to be wishful thinking. Consistent with
the choice we've made elsewhere (functional interfaces are nominal
function types, not structural ones; sealed types are nominal union
types, not structural ones), the rational choice for Java's product
types is also nominal, not structural.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20220810/1f9bad80/attachment.htm>
More information about the amber-dev
mailing list