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