Data Oriented Programming, Beyond Records

Brian Goetz brian.goetz at oracle.com
Sun Jan 18 16:57:48 UTC 2026


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
>
>
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-spec-experts/attachments/20260118/75b11e95/attachment-0001.htm>


More information about the amber-spec-experts mailing list