[External] : Re: Data Oriented Programming, Beyond Records

Viktor Klang viktor.klang at oracle.com
Mon Jan 19 12:21:54 UTC 2026


Note that "thread safe classes" (I'd want to be more precise in wording 
and explicitly call it out as *synchronized classes*) do not guarantee 
atomicity of access over 2+ accessors anyway, and to be able to perform 
such accesses you'd both need to know that the exact implementation is 
using synchronization for concurrency control, AND you'll need to know 
exactly which lock/monitor to obtain.

So that's a whole lot of internal implementation concern which you need 
to deal with, and in that case, you can also decide to synchronize on 
the instance *before/around* the match, and you'll have your atomicity.

On 2026-01-19 08:45, forax at univ-mlv.fr wrote:
>
>
> ------------------------------------------------------------------------
>
>     *From: *"Viktor Klang" <viktor.klang at oracle.com>
>     *To: *"Brian Goetz" <brian.goetz at oracle.com>, "Remi Forax"
>     <forax at univ-mlv.fr>
>     *Cc: *"amber-spec-experts" <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://en.wikipedia.org/wiki/First-class_citizen 
> <https://urldefense.com/v3/__https://en.wikipedia.org/wiki/First-class_citizen__;!!ACWV5N9M2RV99hQ!NN-Sp9RWODO8xckNcMktjEc6QIh40fY7xUZOQfrIF3CoHIV5R5Qk0vNmTfWgoOa4mrk_tmsjVuM173J9BgrG$>
>
> 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, 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
>
>
-- 
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/db1cc6f5/attachment-0001.htm>


More information about the amber-spec-experts mailing list