Data Oriented Programming, Beyond Records
forax at univ-mlv.fr
forax at univ-mlv.fr
Mon Jan 19 18:40:50 UTC 2026
> From: "Brian Goetz" <brian.goetz at oracle.com>
> To: "Remi Forax" <forax at univ-mlv.fr>, "Viktor Klang" <viktor.klang at oracle.com>
> Cc: "amber-spec-experts" <amber-spec-experts at openjdk.java.net>
> Sent: Monday, January 19, 2026 3:55:07 PM
> Subject: Re: Data Oriented Programming, Beyond Records
> Let's try and pull back a bit.
> You've raised two concerns:
> - If carrier components can be mutable, then carriers are subject to the
> lost-in-the-hashmap phenomena.
> - In a mutable carrier using synchronization to guard its state, there is no way
> to enlist its deconstruction pattern in the synchronization protocol.
> Neither of these are concerns that spring directly from mutability, as much as
> "things that just don't happen with immutability." And both of them require a
> number of other things to go wrong:
> - To lose a carrier as a HashMap key / HashSet member, it has to not only be
> mutable, but it has to be placed in a hash-based collection _and then mutated_.
> (And it is well known that using an object as a HashMap key requires a deeper
> understanding of the behavior of that object; trying to enumerate when this is
> safe quickly devolves into a long list with a lot of considerations.)
The modifier "component" is too close to the "property" modifier I wanted to include years ago, it's just to sugary for its own good.
Moreover if javac generates syntactic sugar, the generated code has to be bullet proof and should work in any conditions.
I have used HashSet as an example to say that I would prefer equals/hashCode/toString not to be generated so my students can launch the debugger to understand why it does not work as intended.
BTW, can you add a "component" to an enum ?
enum Pet(boolean dangerous) {
CAT(false), TIGER(true);
private final component boolean dangerous;
}
IMO, adding the component keyword is just a nice to have, not something fundamental that has to be added to a carrier class, it can be added later.
(funnily, C# still think that their properties require more syntactic sugar [1]).
[1] https://learn.microsoft.com/fr-fr/dotnet/csharp/whats-new/csharp-14#the-field-keyword
> - To have a problem with deconstructing a mutable carrier, it would have to have
> cross-fields invariants, and actually be shared across threads, and actually be
> mutated while another class is working with it, while the inspecting class has
> not attempted to participate in its synchronization protocol.
It starts to become OT but a mutable thread-safe class does not have to expose it's synchronisation protocol (the implementation can also use a CAS after all), so why the inspecting class has to be modified ?
As i said earlier, i'm 100% for trying to simplify the deconstructing protocol, but it comes at the cost of having some classes that can not be retrofitted to be pattern-matchable while we know that if the deconstructing protocol is more involved, we can support those classes.
Yes, it's a tradeoff, and i'm not convince this is the right choice, but maybe this is the only corner case and i'm too focus on it.
> But these are all vague "something could go wrong" worries. Do you have a
> concrete point? Are you claiming that there is something wrong with the design
> of this feature because of it? Or are you merely pointing out that "there are
> risks, and we should educate people"?
Nothing is wrong, if the tradeoffs are known and acknowledge.
Rémi
> On 1/19/2026 2:45 AM, [ mailto:forax at univ-mlv.fr | forax at univ-mlv.fr ] wrote:
>>> From: "Viktor Klang" [ mailto:viktor.klang at oracle.com |
>>> <viktor.klang at oracle.com> ]
>>> To: "Brian Goetz" [ mailto:brian.goetz at oracle.com | <brian.goetz at oracle.com> ] ,
>>> "Remi Forax" [ mailto:forax at univ-mlv.fr | <forax at univ-mlv.fr> ]
>>> Cc: "amber-spec-experts" [ mailto:amber-spec-experts at openjdk.java.net |
>>> <amber-spec-experts at openjdk.java.net> ]
>>> Sent: Monday, January 19, 2026 1:08:05 AM
>>> Subject: Re: Data Oriented Programming, Beyond Records
>>> Since someone said synchronized , I was summoned.
>> :)
>>> If a definition of deconstruction is isomorphic to an instance-of check plus
>>> invoking N accessors, it is clear that it would have the same atomicity as
>>> invoking the accessors directly (i.e. none).
>>> I would say that it is much more consistent, than to try to create special-cases
>>> with different atomicity guarantees. Also, there is nothing which prevents
>>> exposing a "shapshot"-method which returns a record (or other carrier class) if
>>> one wants atomicity in matching:
>>> switch (foo) {
>>> case Foo foo when foo.snapshot() instanceof FooSnapshot(var x, var y) ->
>>> }
>>> PS. it's worth noting that this only applies to types which can enforce stable
>>> snapshotting, either via synchronized or via optimistic concurrency control
>>> schemes such as STM.
>> yes,
>> snapshoting is a good term to describe the semantics.
>> Conceptually, you do not want your object to change state in the middle of the
>> pattern matching, so you snapshot it.
>> For me, this is no different from having a value class to guarantee atomicity by
>> default, by default pattern matching should guarantee that you can not see an
>> object in a state that does not exist.
>> I do not like the fact that a user has to call .snapshot() explicitly because it
>> goes against the idea that in Java, a thread safe class is like any other class
>> from the user POV, the maintainer of the class has to do more work, but from
>> the user POV a thread safe class works like any other classes.
>> Here you are asking the thread safe classes to have an extra step at use site
>> when using pattern matching.
>> That why i said that by not providing snapshoting by default, it makes thread
>> safe classes are not first class objects anymore
>> see [
>> https://urldefense.com/v3/__https://en.wikipedia.org/wiki/First-class_citizen__;!!ACWV5N9M2RV99hQ!IeFWOok1gyUoc5rdVpRZeQXWIXZcPNFjfjSd2L3s0TwbW6uBew9GZ4SqAlTBnRIwKmm6pX-XSe-SxTR59tq6$
>> | https://en.wikipedia.org/wiki/First-class_citizen ]
>> regards,
>> Rémi
>>> On 2026-01-18 17:57, Brian Goetz wrote:
>>>> You're trying to make a point, but you're walking all around it and not making
>>>> it directly, so I can only guess at what you mean. I think your point is: "if
>>>> you're going to embrace mutability, you should also embrace thread-safety" (but
>>>> actually, I think you are really trying to argue against mutability, but you
>>>> didn't use any of those words, so again, we're only guessing at what you really
>>>> mean.)
>>>> A mutable carrier can be made thread-safe by protecting the state with locks or
>>>> volatile fields or delegation to thread-safe containers, as your example shows.
>>>> And since pattern matching proceeds through accessors, it will get the benefit
>>>> of those mechanisms to get race-freedom for free. But you want (I think ) more:
>>>> that the dtor not only retrieves the latest value of all the components, but
>>>> that it must be able to do so _atomically_.
>>>> I think the case you are really appealing to is one where there an invariant
>>>> that constrains the state, such as a mutable Range, because then if one
>>>> observed a range mid-update and unpacked the bounds, they could be seen to be
>>>> inconsistent. So let's take that:
>>>> class Range(int lo, int hi) {
>>>> private int lo, hi;
>>>> Range {
>>>> if (lo > hi) throw new IAE();
>>>> }
>>>> // accessors synchronized on known monitor, such as `this`
>>>> }
>>>> Now, while a client will always see up-to-date values for the range components
>>>> (Range is race-free), the worry is that they might see something _too_
>>>> up-to-date, which is to say, seeing a range mid-update:
>>>> case Range(var lo, var hi):
>>>> while another thread does
>>>> sync (monitor of range) { range.lo += 5; range.hi += 5; }
>>>> If the range is initially (0,1), with some bad timing, the client could see
>>>> lo=6, hi=1 get unpacked, and scratch their heads about what happened.
>>>> This is analogous to the case where we have an ArrayList wrapped with a
>>>> synchronizedList; while access to the list's state is race-free, if you want a
>>>> consistent snapshot (such as iterating it), you have to hold the lock for the
>>>> duration of the composite operation. Similarly, if you have a mutable carrier
>>>> that might be concurrently modified, and you care about seeing updates
>>>> atomically, you would have to hold the lock during the pattern match:
>>>> sync (monitor of range) { Range(var lo, var hi) = range; ... use lo/hi ... }
>>>> I think the argument you are (not) making goes like this:
>>>> - We will have no chance to get users to understand that deconstruction is not
>>>> atomic, because it just _looks_ so atomic!
>>>> - Therefore, we either have to find a way so it can be made atomic, OR (I think
>>>> your preference), outlaw mutable carriers in the first place.
>>>> (I really wish you would just say what you mean, rather than making us guess and
>>>> make your arguments for you...)
>>>> While there's a valid argument there to make here (if you actually made it),
>>>> I'll just note that the leap from "something bad and surprising can happen" to
>>>> "so, this design needs to be radically overhauled" is ... quite a leap.
>>>> (This is kind of the same leap you made about hash-based collections: "there is
>>>> a risk, therefore we must neuter the feature so there are no risks." Rather
>>>> than leaping to "so let's change the design center", I would rather have a
>>>> conversation about what risks there _are_, and whether they are acceptable, or
>>>> whether the cure is worse than the disease.)
>>>> On 1/18/2026 7:49 AM, [ mailto:forax at univ-mlv.fr | forax at univ-mlv.fr ] wrote:
>>>>>> From: "Brian Goetz" [ mailto:brian.goetz at oracle.com | <brian.goetz at oracle.com> ]
>>>>>> To: "Remi Forax" [ mailto:forax at univ-mlv.fr | <forax at univ-mlv.fr> ] , "Viktor
>>>>>> Klang" [ mailto:viktor.klang at oracle.com | <viktor.klang at oracle.com> ]
>>>>>> Cc: "amber-spec-experts" [ mailto:amber-spec-experts at openjdk.java.net |
>>>>>> <amber-spec-experts at openjdk.java.net> ]
>>>>>> Sent: Sunday, January 18, 2026 2:00:19 AM
>>>>>> Subject: Re: Data Oriented Programming, Beyond Records
>>>>>> In reality, the deconstructor is not a method at all.
>>>>>> When we match:
>>>>>> x instanceof R(P, Q)
>>>>>> we first ask `instanceof R`, and if that succeeds, we call the accessors for the
>>>>>> first two components. The accessors are instance methods, but the deconstructor
>>>>>> is not embodied as a method. This is true for carriers as well as for records.
>>>>> Pattern matching and late binding are dual only for public types,
>>>>> if an implementation class (the one containing the fields) is not visible from
>>>>> outside, the way to get to fields is by using late binding.
>>>>> If the only way to do the late binding is by using accessors, then you can not
>>>>> guarantee the atomicity of the deconstruction,
>>>>> or said differently the pattern matching will be able to see states that does
>>>>> not exist.
>>>>> Let say I have a public thread safe class containing two fields, and I want see
>>>>> that class has a carrier class,
>>>>> with the idea that a carrier class either provide a deconstructor method or
>>>>> accessors.
>>>>> I can write the following code :
>>>>> public final class ThreadSafeData(String name, int age) {
>>>>> private String name;
>>>>> private int age;
>>>>> private final Object lock = new Object();
>>>>> public ThreadSafeData(String name, int age) {
>>>>> synchronized(lock) {
>>>>> this.name = name;
>>>>> this.age = age;
>>>>> }
>>>>> }
>>>>> public void set(String name, int age) {
>>>>> synchronized(lock) {
>>>>> this.name = name;
>>>>> this.age = age;
>>>>> }
>>>>> }
>>>>> public String toString() {
>>>>> synchronized(lock) {
>>>>> return name + " " + age;
>>>>> }
>>>>> }
>>>>> public deconstructor() {. // no return type, the compiler checks that the return
>>>>> values have the same carrier definition
>>>>> record Tuple(String name, int age) { }
>>>>> synchronized(lock) {
>>>>> return new Tuple(name, age);
>>>>> }
>>>>> }
>>>>> // no accessors here, if you want to have access the state, use pattern matching
>>>>> like this
>>>>> // ThreadSafeHolder holder = ...
>>>>> // ThreadSafeHolder(String name, int age) = holder;
>>>>> }
>>>>> I understand that you are trying to drastically simplify the pattern matching
>>>>> model (yai !) by removing the deconstructor method but by doing that you are
>>>>> making thread safe classes second class citizens.
>>>>> regards,
>>>>> Rémi
>>>>>> On 1/17/2026 5:09 PM, [ mailto:forax at univ-mlv.fr | forax at univ-mlv.fr ] wrote:
>>>>>>>> From: "Viktor Klang" [ mailto:viktor.klang at oracle.com |
>>>>>>>> <viktor.klang at oracle.com> ]
>>>>>>>> To: "Remi Forax" [ mailto:forax at univ-mlv.fr | <forax at univ-mlv.fr> ] , "Brian
>>>>>>>> Goetz" [ mailto:brian.goetz at oracle.com | <brian.goetz at oracle.com> ]
>>>>>>>> Cc: "amber-spec-experts" [ mailto:amber-spec-experts at openjdk.java.net |
>>>>>>>> <amber-spec-experts at openjdk.java.net> ]
>>>>>>>> Sent: Saturday, January 17, 2026 5:00:41 PM
>>>>>>>> Subject: Re: Data Oriented Programming, Beyond Records
>>>>>>>> Just a quick note regarding the following, given my experience in this area:
>>>>>>>> On 2026-01-17 11:36, Remi Forax wrote:
>>>>>>>>> A de-constructor becomes an instance method that must return a carrier
>>>>>>>>> class/carrier interface, a type that has the information to be destructured and
>>>>>>>>> the structure has to match the one defined by the type.
>>>>>>> Hello Viktor,
>>>>>>> thanks to bring back that point,
>>>>>>>> This simply does not work as a deconstructor cannot be an instance-method just
>>>>>>>> like a constructor cannot be an instance method: It strictly belongs to the
>>>>>>>> type itself (not the hierarchy) and
>>>>>>> It can work as you said for a concrete type, but for an abstract type, you need
>>>>>>> to go from the abstract definition to the concrete one,
>>>>>>> if you do not want to re-invent the wheel here, the deconstructor has to be an
>>>>>>> abstract instance method.
>>>>>>> For example, with a non-public named implementation
>>>>>>> interface Pair<F, S>(F first, S second) {
>>>>>>> public <F,S> Pair<F,S> of(F first, S second) {
>>>>>>> record Impl<F, S>(F first, S second) implements Pair<F, S>{ }
>>>>>>> return new Impl<>(first, second);
>>>>>>> }
>>>>>>> }
>>>>>>> inside Pair, there is no concrete field first and second, so you need a way to
>>>>>>> extract them from the implementation.
>>>>>>> This can be implemented either using accessors (first() and second()) but you
>>>>>>> have a problem if you want your implementation to be mutable and synchronized
>>>>>>> on a lock (because the instance can be changed in between the call to first()
>>>>>>> and the call to second()) or you can have one abstract method, the
>>>>>>> deconstructor.
>>>>>>>> it doesn't play well with implementing multiple interfaces (name clashing), and
>>>>>>>> interacts poorly with overload resolution (instead of choosing most-specific,
>>>>>>>> you need to select a specific point in the hierarchy to call the method).
>>>>>>> It depends on the compiler translation, but if you limit yourself to one
>>>>>>> destructor per class (the dual of the canonical constructor), the deconstructor
>>>>>>> can be desugared to one instance method that takes nothing and return
>>>>>>> java.lang.Object, so no name clash and no problem of overloading (because
>>>>>>> overloading is not allowed, you have to use '_' at use site).
>>>>>>>> --
>>>>>>>> Cheers,
>>>>>>>> √
>>>>>>> regards,
>>>>>>> Rémi
>>>>>>>> Viktor Klang
>>>>>>>> Software Architect, Java Platform Group
>>>>>>>> Oracle
>>> --
>>> Cheers,
>>> √
>>> Viktor Klang
>>> Software Architect, Java Platform Group
>>> Oracle
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-spec-experts/attachments/20260119/b0de0710/attachment-0001.htm>
More information about the amber-spec-experts
mailing list