Data Oriented Programming, Beyond Records

Viktor Klang viktor.klang at oracle.com
Mon Jan 19 00:08:05 UTC 2026


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.

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, forax at univ-mlv.fr wrote:
>>
>>
>> ------------------------------------------------------------------------
>>
>>     *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: *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, forax at univ-mlv.fr wrote:
>>
>>
>>
>>         ------------------------------------------------------------------------
>>
>>             *From: *"Viktor Klang" <viktor.klang at oracle.com>
>>             *To: *"Remi Forax" <forax at univ-mlv.fr>, "Brian Goetz"
>>             <brian.goetz at oracle.com>
>>             *Cc: *"amber-spec-experts"
>>             <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/ed0a42d3/attachment-0001.htm>


More information about the amber-spec-experts mailing list