From gavin.bierman at oracle.com Fri Jan 9 23:08:05 2026 From: gavin.bierman at oracle.com (Gavin Bierman) Date: Fri, 9 Jan 2026 23:08:05 +0000 Subject: Amber features 2026 Message-ID: <1C8344A8-B712-422D-9F66-FB4ED89373FC@oracle.com> Dear spec experts, Happy New Year to you all! We thought this was a good time to share you some of the thinking regarding Amber features for 2026. Currently we have one feature in preview - Primitive Patterns. We?d love to get more feedback on this feature - please keep kicking the tires! We plan two new features in the near term. Draft JEPs are being worked on and will be released as soon as possible. But here are some brief details while you are waiting for the draft JEPs (in the name of efficiency, *please* let's save discussion for that point). ## PATTERN ASSIGNMENT Pattern matching is an inherently partial process: a value either matches a pattern, or it does not. But sometimes, we know that the pattern will always match; and we are using the pattern matching process as a convenient means to disassemble a value, for example: record ColorPoint(int x, int y, RGB color) {} void somethingImportant(ColorPoint cp) { if (cp instanceof ColorPoint(var x, var y, var c)) { // important code } } The use of pattern matching is great, but the fact that we have to use it in a conditional statement is annoying. It?s clutter, and worse, it is making something known by the developer and compiler look as if it were unknown; and, as a consequence, the important code ends up being indented and the scope of the pattern variables is limited to the then block. The indent-adverse developer may reach for the following, but it?s hardly better: void somethingImportant(ColorPoint cp) { if (!(cp instanceof ColorPoint(var x, var y, var c))) { return; } // important code } The real issue here is that both the developer and the compiler can see that the pattern matching is not partial - it will always succeed - but we have no way of recording this semantic information. What we really want is a form of assignment where the left-hand-side is not a variable but a **pattern**. So, we can rewrite our method as follows: void somethingImportant(ColorPoint cp) { ColorPoint(var x, var y, var c) = cp; // Pattern Assignment! // important code } Luckily, the spec already defines what it means for a pattern to be unconditional (JLS 14.30.3), so we can build on this void hopeful(Object o) { ColorPoint(var x, var y, var c) = o; // Compile-time error! } ## CONSTANT PATTERNS Another common pattern (sic) with pattern matching code is where we want to match a particular pattern but only for a certain value, for example: void code(Shape s) { switch (s) { case Point(var x, var y) when x == 0 && y == 0 ?> { // special code for origin } case Point(var x, var y) -> { // code for non-origin points } ... } ... } It?s great that our pattern `switch` allows us to have separate clauses for the point on the origin and the other points. But it?s a shame that we have to use a `when` clause to specify the constant values for the origin point. This makes code less readable and is not what we would do if we were thinking of the code more mathematically. What we want to do is inline the zero values into the pattern itself, i.e. void code(Shape s) { switch (s) { case Point(0, 0) -> { // special code for origin } case Point(var x, var y) -> { // code for non-origin points } ... } ... } In other words, we?d like to support a subset of constant expressions, including `null`, to appear as nested patterns. We think that will lead to even more readable and concise pattern matching code and, from a language design perspective, allows us to address the somewhat awkward separation of case constants and case patterns, by making (almost) everything a pattern. We have other new Amber features in the pipeline, but we propose to prioritize these two features, along with the primitive patterns feature already in preview. Please look out for the announcements of draft JEPs when they arrive - as always, we value enormously your help in designing new features for our favorite programming language! Wishing you a happy and successful 2026! Gavin From brian.goetz at oracle.com Tue Jan 13 21:52:47 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Tue, 13 Jan 2026 16:52:47 -0500 Subject: Data Oriented Programming, Beyond Records Message-ID: Here's a snapshot of where my head is at with respect to extending the record goodies (including pattern matching) to a broader range of classes, deconstructors for classes and interfaces, and compatible evolution of records. Hopefully this will unblock quite a few things. As usual, let's discuss concepts and directions rather than syntax. # Data-oriented Programming for Java: Beyond records Everyone loves records; they allow us to create shallowly immutable data holder classes -- which we can think of as "nominal tuples" -- derived from a concise state description, and to destructure records through pattern matching.? But records have strict constraints, and not all data holder classes fit into the restrictions of records.? Maybe they have some mutable state, or derived or cached state that is not part of the state description, or their representation and their API do not match up exactly, or they need to break up their state across a hierarchy.? In these classes, even though they may also be ?data holders?, the user experience is like falling off a cliff.? Even a small deviation from the record ideal means one has to go back to a blank slate and write explicit constructor declarations, accessor method declarations, and Object method implementations -- and give up on destructuring through pattern matching. Since the start of the design process for records, we?ve kept in mind the goal of enabling a broader range of classes to gain access to the "record goodies": reduced declaration burden, participating in destructuring, and soon, [reconstruction](https://openjdk.org/jeps/468). During the design of records, we also explored a number of weaker semantic models that would allow for greater flexibility. While at the time they all failed to live up to the goals _for records_, there is a weaker set of semantic constraints we can impose that allows for more flexibility and still enables the features we want, along with some degree of syntactic concision that is commensurate with the distance from the record-ideal, without fall-off-the-cliff behaviors. Records, sealed classes, and destructuring with record patterns constitute the first feature arc of "data-oriented programming" for Java.? After considering numerous design ideas, we're now ready to move forward with the next "data oriented programming" feature arc: _carrier classes_ (and interfaces.) ## Beyond record patterns Record patterns allow a record instance to be destructured into its components. Record patterns can be used in `instanceof` and `switch`, and when a record pattern is also exhaustive, will be usable in the upcoming [_pattern assignment statement_](https://mail.openjdk.org/pipermail/amber-spec-experts/2026-January/004306.html) feature. In exploring the question "how will classes be able to participate in the same sort of destructuring as records", we had initially focused on a new form of declaration in a class -- a "deconstructor" -- that operated as a constructor in reverse. Just as a constructor takes component values and produces an aggregate instance, a deconstructor would take an aggregate instance and recover its component values. But as this exploration played out, the more interesting question turned out to be: which classes are suitable for destructuring in the first place? And the answer to that question led us to a different approach for expressing deconstruction.? The classes that are suitable for destructuring are those that, like records, are little more than carriers for a specific tuple of data. This is not just a thing that a class _has_, like a constructor or method, but something a class _is_.? And as such, it makes more sense to describe deconstruction as a top-level property of a class.? This, in turn, leads to a number of simplifications. ## The power of the state description Records are a semantic feature; they are only incidentally concise.? But they _are_ concise; when we declare a record ? ? record Point(int x, int y) { ... } we automatically get a sensible API (canonical constructor, deconstruction pattern, accessor methods for each component) and implementation (fields, constructor, accessor methods, Object methods.)? We can explicitly specify most of these (except the fields) if we like, but most of the time we don't have to, because the default is exactly what we want. A record is a shallowly-immutable, final class whose API and representation are _completely defined_ by its _state description_.? (The slogan for records is "the state, the whole state, and nothing but the state.")? The state description is the ordered list of _record components_ declared in the record's header.? A component is more than a mere field or accessor method; it is an API element on its own, describing a state element that instances of the class have. The state description of a record has several desirable properties: ?- The components in the order specified, are the _canonical_ description of the ? ?record's state. ?- The components are the _complete_ description of the record?s state. ?- The components are _nominal_; their names are a committed part of the ? ?record's API. Records derive their benefits from making two commitments: ?- The _external_ commitment that the data-access API of a record (constructor, ? ?deconstruction pattern, and component accessor methods) is defined by the ? ?state description. ?- The _internal_ commitments that the _representation_ of the record (its ? ?fields) is also completely defined by the state description. These semantic properties are what enable us to derive almost everything about records.? We can derive the API of the canonical constructor because the state description is canonical.? We can derive the API for the component accessor methods because the state description is nominal.? And we can derive a deconstruction pattern from the accessor methods because the state description is complete (along with sensible implementations for the state-related `Object` methods.) The internal commitment that the state description is also the representation allows us to completely derive the rest of the implementation. Records get a (private, final) field for each component, but more importantly, there is a clear mapping between these fields and their corresponding components, which is what allows us to derive the canonical constructor and accessor method implementations. Records can additionally declare a _compact constructor_ that allows us to elide the boilerplate aspects of record constructors -- the argument list and field assignments -- and just specify the code that is _not_ mechanically derivable. This is more concise, less error-prone, and easier to read: ? ? record Rational(int num, int denom) { ? ? ? ? Rational { ? ? ? ? ? ? if (denom == 0) ? ? ? ? ? ? ? ? throw new IllegalArgumentException("denominator cannot be zero"); ? ? ? ? } ? ? } is shorthand for the more explicit ? ? record Rational(int num, int denom) { ? ? ? ? Rational(int num, int denom) { ? ? ? ? ? ? if (denom == 0) ? ? ? ? ? ? ? ? throw new IllegalArgumentException("denominator cannot be zero"); ? ? ? ? ? ? this.num = num; ? ? ? ? ? ? this.denom = denom; ? ? ? ? } ? ? } While compact constructors are pleasantly concise, the more important benefit is that by eliminating the mechanically derivable code, the "more interesting" code comes to the fore. Looking ahead, the state description is a gift that keeps on giving.? These semantic commitments are enablers for a number of potential future language and library features for managing object lifecycle, such as: ?- [Reconstruction](https://openjdk.org/jeps/468) of record instances, allowing ? ?the appearance of controlled mutation of record state. ?- Automatic marshalling and unmarshalling of record instances. ?- Instantiating or destructuring record instances identifying components ? ?nominally rather than positionally. ### Reconstruction JEP 468 proposes a mechanism by which a new record instance can be derived from an existing one using syntax that is evocative of direct mutation, via a `with` expression: ? ? record Complex(double re, double im) { } ? ? Complex c = ... ? ? Complex cConjugate = c with { im = -im; }; The block on the right side of `with` can contain any Java statements, not just assignments.? It is enhanced with mutable variables (_component variables_) for each component of the record, initialized to the value of that component in the record instance on the left, the block is executed, and a new record instance is created whose component values are the ending values of the component variables. A reconstruction expression implicitly destructures the record instance using the canonical deconstruction pattern, executes the block in a scope enhanced with the component variables, and then creates a new record using the canonical constructor.? Invariant checking is centralized in the canonical constructor, so if the new state is not valid, the reconstruction will fail.? JEP 468 has been "on hold" for a while, primarily because we were waiting for sufficient confidence that there was a path to extending it to suitable classes before committing to it for records.? The ideal path would be for those classes to also support a notion of canonical constructor and deconstruction pattern. Careful readers will note a similarity between the transformation block of a `with` expression and the body of a compact constructor.? In both cases, the block is "preloaded" with a set of component variables, initialized to suitable starting values, the block can mutate those variables as desired, and upon normal completion of the block, those variables are passed to a canonical constructor to produce the final result.? The main difference is where the starting values come from; for a compact constructor, it is from the constructor parameters, and for a reconstruction expression, it is from the canonical deconstruction pattern of the source record to the left of `with`. ### Breaking down the cliff Records make a strong semantic commitment to derive both their API and representation from the state description, and in return get a lot of help from the language.? We can now turn our attention to smoothing out "the cliff" -- identifying weaker semantic commitments that classes can make that would still allow classes to get _some_ help from the language.? And ideally, the amount of help you give up would be proportional to the degree of deviation from the record ideal. With records, we got a lot of mileage out of having a complete, canonical, nominal state description.? Where the record contract is sometimes too constraining is the _implementation_ contract that the representation aligns exactly with the state description, that the class is final, that the fields are final, and that the class may not extend anything but `Record`. Our path here takes one step back and one step forward: keeping the external commitment to the state description, but dropping the internal commitment that the state description _is_ the representation -- and then _adding back_ a simple mechanism for mapping fields representing components back to their corresponding components, where practical.? (With records, because we derive the representation from the state description, this mapping can be safely inferred.) As a thought experiment, imagine a class that makes the external commitment to a state description -- that the state description is a complete, canonical, nominal description of its state -- but is on its own to provide its representation.? What can we do for such a class?? Quite a bit, actually.? For all the same reasons we can for records, we can derive the API requirement for a canonical constructor and component accessor methods.? From there, we can derive both the requirement for a canonical deconstruction pattern, and also the implementation of the deconstruction pattern (as it is implemented in terms of the accessor methods). And since the state description is complete, we can further derive sensible default implementations of the Object methods `equals`, `hashCode`, and `toString` in terms of the accessor methods as well. And given that there is a canonical constructor and deconstruction pattern, it can also participate in reconstruction.? The author would just have to provide the fields, accessor methods, and canonical constructor.? This is good progress, but we'd like to do better. What enables us to derive the rest of the implementation for records (fields, constructor, accessor methods, and Object methods) is the knowledge of how the representation maps to the state description.? Records commit to their state description _being_ the representation, so is is a short leap from there to a complete implementation. To make this more concrete, let's look at a typical "almost record" class, a carrier for the state description `(int x, int y, Optional s)` but which has made the representation choice to internally store `s` as a nullable `String`. ``` class AlmostRecord { ? ? private final int x; ? ? private final int y; ? ? private final String s;? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// * ? ? public AlmostRecord(int x, int y, Optional s) { ? ? ? ? this.x = x; ? ? ? ? this.y = y; ? ? ? ? this.s = s.orElse(null);? ? ? ? ? ? ? ? ? ? ? ? ? ? // * ? ? } ? ? public int x() { return x; } ? ? public int y() { return y; } ? ? public Optional s() { ? ? ? ? return Optional.ofNullable(s);? ? ? ? ? ? ? ? ? ? ? // * ? ? } ? ? public boolean equals(Object other) { ... }? ? ?// derived from x(), y(), s() ? ? public int hashCode() { ... }? ? ? ? ? ? ? ? ? ?//? ? " ? ? public String toString() { ... }? ? ? ? ? ? ? ? //? ? " } ``` The main differences between this class and the expansion of its record analogue are the lines marked with a `*`; these are the ones that deal with the disparity between the state description and the actual representation.? It would be nice if the author of this class _only_ had to write the code that was different from what we could derive for a record; not only would this be pleasantly concise, but it would mean that all the code that _is_ there exists to capture the differences between its representation and its API. ## Carrier classes A _carrier class_ is a normal class declared with a state description.? As with a record, the state description is a complete, canonical, nominal description of the class's state.? In return, the language derives the same API constraints as it does for records: canonical constructor, canonical deconstruction pattern, and component accessor methods. ? ?class Point(int x, int y) {? ? ? ? ? ? ? ? // class, not record! ? ? ? ?// explicitly declared representation ? ? ? ?... ? ? ? ?// must have a constructor taking (int x, int y) ? ? ? ?// must have accessors for x and y ? ? ? ?// supports a deconstruction pattern yielding (int x, int y) ? ?} Unlike a record, the language makes no assumptions about the object's representation; the class author has to declare that just as with any other class. Saying the state description is "complete" means that it carries all the ?important? state of the class -- if we were to extract this state and recreate the object, that should yield an ?equivalent? instance.? As with records, this can be captured by tying together the behavior of construction, accessors, and equality: ``` Point p = ... Point q = new Point(p.x(), p.y()); assert p.equals(q); ``` We can also derive _some_ implementation from the information we have so far; we can derive sensible implementations of the `Object` methods (implemented in terms of component accessor methods) and we can derive the canonical deconstruction pattern (again in terms of the component accessor methods).? And from there, we can derive support for reconstruction (`with` expressions.) Unfortunately, we cannot (yet) derive the bulk of the state-related implementation: the canonical constructor and component accessor methods. ### Component fields and accessor methods One of the most tedious aspects of data-holder classes is the accessor methods; there are often many of them, and they are almost always pure boilerplate.? Even though IDEs can reduce the writing burden by generating these for us, readers still have to slog through a lot of low-information code -- just to learn that they didn't actually need to slog through that code after all.? We can derive the implementation of accessor methods for records because records make the internal commitment that the components are all backed with individual fields whose name and type align with the state description. For a carrier class, we don't know whether _any_ of the components are directly backed by a single field that aligns to the name or type of the component.? But it is a pretty good bet that many carrier class components will do exactly this for at least _some_ of their fields.? If we can tell the language that this correspondence is not merely accidental, the language can do more for us. We do so by allowing suitable fields of a carrier class to be declared as `component` fields.? (As usual at this stage, syntax is provisional, but not currently a topic for discussion.)? A component field must have the same name and type as a component of the current class (though it need not be `private` or `final`, as record fields are.)? This signals that this field _is_ the representation for the corresponding component, and hence we can derive the accessor method for this component as well. ``` class Point(int x, int y) { ? ? private /* mutable */ component int x; ? ? private /* mutable */ component int y; ? ? // must have a canonical constructor, but (so far) must be explicit ? ? public Point(int x, int y) { ? ? ? ? this.x = x; ? ? ? ? this.y = y; ? ? } ? ? // derived implementations of accessors for x and y ? ? // derived implementations of equals, hashCode, toString } ``` This is getting better; the class author had to bring the representation and the mapping from representation to components (in the form of the `component` modifier), and the canonical constructor. ### Compact constructors Just as we are able to derive the accessor method implementation if we are given an explicit correspondence between a field and a component, we can do the same for constructors.? For this, we build on the notion of _compact constructors_ that was introduced for records. As with a record, a compact constructor in a carrier class is a shorthand for a canonical constructor, which has the same shape as the state description, but which is freed of the responsibility of actually committing the ending value of the component parameters to the fields.? The main difference is that for a record, _all_ of the components are backed by a component field, whereas for a carrier class, only some of them might be.? But we can generalize compact constructors by freeing the author of the responsibility to initialize the _component_ fields, while leaving them responsible for initializing the rest of the fields.? In the limiting case where all components are backed by component fields, and there is no other logic desired in the constructor, the compact constructor may be elided. For our mutable `Point` class, this means we can elide nearly everything, except the field declarations themselves: ``` class Point(int x, int y) { ? ? private /* mutable */ component int x; ? ? private /* mutable */ component int y; ? ? // derived compact constructor ? ? // derived accessors for x, y ? ? // derived implementations of equals, hashCode, toString } ``` We can think of this class as having an implicit empty compact constructor, which in turn means that the component fields `x` and `y` are initialized from their corresponding constructor parameters.? There are also implicitly derived accessor methods for each component, and implementations of `Object` methods based on the state description. This is great for a class where all the components are backed by fields, but what about our `AlmostRecord` class?? The story here is good as well; we can derive the accessor methods for the components backed by component fields, and we can elide the initialization of the component fields from the compact constructor, meaning that we _only_ have to specify the code for the parts that deviate from the "record ideal": ``` class AlmostRecord(int x, ? ? ? ? ? ? ? ? ? ?int y, ? ? ? ? ? ? ? ? ? ?Optional s) { ? ? private final component int x; ? ? private final component int y; ? ? private final String s; ? ? public AlmostRecord { ? ? ? ? this.s = s.orElse(null); ? ? ? ? // x and y fields implicitly initialized ? ? } ? ? public Optional s() { ? ? ? ? return Optional.ofNullable(s); ? ? } ? ? // derived implementation of x and y accessors ? ? // derived implementation of equals, hashCode, toString } ``` Because so many real-world almost-records differ from their record ideal in minor ways, we expect to get a significant concision benefit for most carrier classes, as we did for `AlmostRecord`.? As with records, if we want to explicitly implement the constructor, accessor methods, or `Object` methods, we are still free to do so. ### Derived state One of the most frequent complaints about records is the inability to derive state from the components and cache it for fast retrieval.? With carrier classes, this is simple: declare a non-component field for the derived quantity, initialize it in the constructor, and provide an accessor: ``` class Point(int x, int y) { ? ? private final component int x; ? ? private final component int y; ? ? private final double norm; ? ? Point { ? ? ? ? norm = Math.hypot(x, y); ? ? } ? ? public double norm() { return norm; } ? ? // derived implementation of x and y accessors ? ? // derived implementation of equals, hashCode, toString } ``` ### Deconstruction and reconstruction Like records, carrier classes automatically acquire deconstruction patterns that match the canonical constructor, so we can destructure our `Point` class as if it were a record: ? ? case Point(var x, var y): Because reconstruction (`with`) derives from a canonical constructor and corresponding deconstruction pattern, when we support reconstruction of records, we will also be able to do so for carrier classes: ? ? point = point with { x = 3; } ## Carrier interfaces A state description makes sense on interfaces as well.? It makes the statement that the state description is a complete, canonical, nominal description of the interface's state (subclasses are allowed to add additional state), and accordingly, implementations must provide accessor methods for the components. This enables such interfaces to participate in pattern matching: ``` interface Pair(T first, U second) { ? ? // implicit abstract accessors for first() and second() } ... if (o instanceof Pair(var a, var b)) { ... } ``` Along with the upcoming feature for pattern assignment in foreach-loop headers, if `Map.Entry` became a carrier interface (which it will), we would be able to iterate a `Map` like: ? ? for (Map.Entry(var key, var val) : map.entrySet()) { ... } It is a common pattern in libraries to export an interface that is sealed to a single private implementation.? In this pattern, the interface and implementation can share a common state description: ``` public sealed interface Pair(T first, U second) { } private record PairImpl(T first, U second) implements Pair { } ``` Compared to the old way of doing this, we get enhanced semantics, better type checking, and more concision. ### Extension The main obligation of a carrier class author is to ensure that the fundamental claim -- that the state description is a complete, canonical, nominal description of the object's state -- is actually true.? This does not rule out having the representation of a carrier class spread out over a hierarchy, so unlike records, carrier classes are not required to be final or concrete, nor are they restricted in their extension. There are several cases that arise when carrier classes can participate in extension: ?- A carrier class extends a non-carrier class; ?- A non-carrier class extends a carrier class; ?- A carrier class extends another carrier class, where all of the superclass ? ?components are subsumed by the subclass state description; ?- A carrier class extends another carrier class, but there are one or more ? ?superclass components that are not subsumed by the subclass state ? ?description. Extending a non-carrier class with a carrier class will usually be motiviated by the desire to "wrap" a state description around an existing hierarchy which we cannot or do not want to modify directly, but we wish to gain the benefits of deconstruction and reconstruction.? Such an implementation would have to ensure that the class actually conforms to the state description, and that the canonical constructor and component accessors are implemented. When one carrier class extends another, the more straightforward case is that it simply adds new components to the state description of the superclass.? For example, given our `Point` class: ``` class Point(int x, int y) { ? ? component int x; ? ? component int y; ? ? // everything else for free! } ``` we can use this as the base class for a 3d point class: ``` class Point3d(int x, int y, int z) extends Point { ? ? component int z; ? ? Point3d { ? ? ? ? super(x, y); ? ? } } ``` In this case -- because the superclass components are all part of the subclass state description -- we can actually omit the constructor as well, because we can derive the association between subclass components and superclass components, and thereby derive the needed super-constructor invocation.? So we could actually write: ``` class Point3d(int x, int y, int z) extends Point { ? ? component int z; ? ? // everything else for free! } ``` One might think that we would need some marking on the `x` and `y` components of `Point3d` to indicate that they map to the corresponding components of `Point`, as we did for associating component fields with their corresponding components. But in this case, we need no such marking, because there is no way that an `int x` component of `Point` and an `int x` component of its subclass could possibly refer to different things -- since they both are tied to the same `int x()` accessor methods.? So we can safely infer which subclass components are managed by superclasses, just by matching up their names and types. In the other carrier-to-carrier extension case, where one or more superclass components are _not_ subsumed by the subclass state description, it is necessary to provide an explicit `super` constructor call in the subclass constructor. A carrier class may be also declared abstract; the main effect of this is that we will not derive `Object` method implementations, instead leaving that for the subclass to do. ### Abstract records This framework also gives us an opportunity to relax one of the restrictions on records: that records can't extend anything other than `java.lang.Record`.? We can also allow records to be declared `abstract`, and for records to extend abstract records. Just as with carrier classes that extend other carrier classes, there are two cases: when the component list of the superclass is entirely contained within that of the subclass, and when one or more superclass components are derived from subclass components (or are constant), but are not components of the subclass itself.? And just as with carrier classes, the main difference is whether an explicit `super` call is required in the subclass constructor. When a record extends an abstract record, any components of the subclass that are also components of the superclass do not implicitly get component fields in the subclass (because they are already in the superclass), and they inherit the accessor methods from the superclass. ### Records are carriers too With this framework in place, records can now be seen to be "just" carrier classes that are implicitly final, extend `java.lang.Record`, that implicitly have private final component fields for each component, and can have no other fields. ## Migration compatibility There will surely be some existing classes that would like to become carrier classes.? This is a compatible migration as long as none of the mandated members conflict with existing members of the class, and the class adheres to the requirement that the state description is a complete, canonical, and nominal description of the object state. ### Compatible evolution of records and carrier classes To date, libraries have been reluctant to use records in public APIs because of the difficulty of evolving them compatibly.? For a record: ``` record R(A a, B b) { } ``` that wants to evolve by adding new components: ``` record R(A a, B b, C c, D d) { } ``` we have several compatibility challenges to manage.? As long as we are only adding and not removing/renaming, accessor method invocations will continue to work. And existing constructor invocations can be allowed to continue work by explicitly adding back a constructor that has the old shape: ``` record R(A a, B b, C c, D d) { ? ? // Explicit constructor for old shape required ? ? public R(A a, B b) { ? ? ? ? this(a, b, DEFAULT_C, DEFAULT_D); ? ? } } ``` But, what can we do about existing uses of record _patterns_? While the translation of record patterns would make adding components binary-compatible, it would not be source-compatible, and there is no way to explicitly add a deconstruction pattern for the old shape as we did with the constructor. We can take advantage of the simplification offered by there being _only_ the canonical deconstruction pattern, and allow uses of deconstruction patterns to supply nested patterns for any _prefix_ of the component list.? So for the evolved record R: ? ? case R(P1, P2) would be interpreted as: ? ? case R(P1, P2, _, _) where `_` is the match-all pattern.? This means that one can compatibly evolve a record by only adding new components at the end, and adding a suitable constructor for compatibility with existing constructor invocations. -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Wed Jan 14 18:17:26 2026 From: forax at univ-mlv.fr (Remi Forax) Date: Wed, 14 Jan 2026 19:17:26 +0100 (CET) Subject: Amber features 2026 In-Reply-To: <1C8344A8-B712-422D-9F66-FB4ED89373FC@oracle.com> References: <1C8344A8-B712-422D-9F66-FB4ED89373FC@oracle.com> Message-ID: <474538796.15268341.1768414646795.JavaMail.zimbra@univ-eiffel.fr> ----- Original Message ----- > From: "Gavin Bierman" > To: "amber-spec-experts" > Sent: Saturday, January 10, 2026 12:08:05 AM > Subject: Amber features 2026 > Dear spec experts, > > Happy New Year to you all! We thought this was a good time to share you some of > the thinking regarding Amber features for 2026. > > Currently we have one feature in preview - Primitive Patterns. We?d love to get > more feedback on this feature - please keep kicking the tires! > > We plan two new features in the near term. Draft JEPs are being worked on and > will be released as soon as possible. But here are some brief details while you > are waiting for the draft JEPs (in the name of efficiency, *please* let's save > discussion for that point). > > ## PATTERN ASSIGNMENT > > Pattern matching is an inherently partial process: a value either matches a > pattern, or it does not. But sometimes, we know that the pattern will always > match; and we are using the pattern matching process as a convenient means to > disassemble a value, for example: > > record ColorPoint(int x, int y, RGB color) {} > > void somethingImportant(ColorPoint cp) { > if (cp instanceof ColorPoint(var x, var y, var c)) { > // important code > } > } > > The use of pattern matching is great, but the fact that we have to use it in a > conditional statement is annoying. It?s clutter, and worse, it is making > something known by the developer and compiler look as if it were unknown; and, > as a consequence, the important code ends up being indented and the scope of > the pattern variables is limited to the then block. The indent-adverse > developer may reach for the following, but it?s hardly better: > > void somethingImportant(ColorPoint cp) { > if (!(cp instanceof ColorPoint(var x, var y, var c))) { > return; > } > // important code > } > > The real issue here is that both the developer and the compiler can see that the > pattern matching is not partial - it will always succeed - but we have no way > of recording this semantic information. > > What we really want is a form of assignment where the left-hand-side is not a > variable but a **pattern**. So, we can rewrite our method as follows: > > void somethingImportant(ColorPoint cp) { > ColorPoint(var x, var y, var c) = cp; // Pattern Assignment! > // important code > } > > Luckily, the spec already defines what it means for a pattern to be > unconditional (JLS 14.30.3), so we can build on this > > void hopeful(Object o) { > ColorPoint(var x, var y, var c) = o; // Compile-time error! > } > > Doing the advent of code of last December, I miss that feature :) But I'm still ambivalent about that feature, for me, it looks like we are missing the big picture. Every time i've talked about this feature in JUGs, one of the questions was why do we need to indicate the type given that the compiler knows it. For example void hopeful(ColorPoint cp) { (var x, var y, var c) = cp; // instead of ColorPoint(var x, var y, var c) = cp; } I wonder if the general question hidden behind is why is it a pattern assignment and not a de-structuration like in other languages. Let's take another example, in other languages, one can write swap like this int a = ... int b = ... (b, a) = (a, b); The equivalent would be record Pair(int first, int second) {} int a = ... int b = ... Pair(var x, var y) = new Pair(a, b); a = x; b = y; Or should we support assignment without the creation of new bindings ? record Pair(int first, int second) {} int a = ... int b = ... Pair(b, a) = new Pair(a, b); Or maybe, this feature should be named pattern declaration and not pattern assignment ? For me, this feature is about de-structuring assignment but by seeing through the keyhole of patterns, i'm fearing we are missing the big picture here. regards, R?mi From forax at univ-mlv.fr Wed Jan 14 18:29:21 2026 From: forax at univ-mlv.fr (Remi Forax) Date: Wed, 14 Jan 2026 19:29:21 +0100 (CET) Subject: Amber features 2026 In-Reply-To: <1C8344A8-B712-422D-9F66-FB4ED89373FC@oracle.com> References: <1C8344A8-B712-422D-9F66-FB4ED89373FC@oracle.com> Message-ID: <1897490063.15291177.1768415361906.JavaMail.zimbra@univ-eiffel.fr> ----- Original Message ----- > From: "Gavin Bierman" > To: "amber-spec-experts" > Sent: Saturday, January 10, 2026 12:08:05 AM > Subject: Amber features 2026 > Dear spec experts, > > Happy New Year to you all! We thought this was a good time to share you some of > the thinking regarding Amber features for 2026. > > Currently we have one feature in preview - Primitive Patterns. We?d love to get > more feedback on this feature - please keep kicking the tires! > > We plan two new features in the near term. Draft JEPs are being worked on and > will be released as soon as possible. But here are some brief details while you > are waiting for the draft JEPs (in the name of efficiency, *please* let's save > discussion for that point). > [...] > > ## CONSTANT PATTERNS > > Another common pattern (sic) with pattern matching code is where we want to > match a particular pattern but only for a certain value, for example: > > void code(Shape s) { > switch (s) { > case Point(var x, var y) when x == 0 && y == 0 ?> { // special code for origin > } > case Point(var x, var y) -> { // code for non-origin points > } > ... > } > ... > } > > It?s great that our pattern `switch` allows us to have separate clauses for the > point on the origin and the other points. But it?s a shame that we have to use > a `when` clause to specify the constant values for the origin point. This makes > code less readable and is not what we would do if we were thinking of the code > more mathematically. What we want to do is inline the zero values into the > pattern itself, i.e. > > void code(Shape s) { > switch (s) { > case Point(0, 0) -> { // special code for origin > } > case Point(var x, var y) -> { // code for non-origin points > } > ... > } > ... > } > > In other words, we?d like to support a subset of constant expressions, including > `null`, to appear as nested patterns. We think that will lead to even more > readable and concise pattern matching code and, from a language design > perspective, allows us to address the somewhat awkward separation of case > constants and case patterns, by making (almost) everything a pattern. Hello Gavin, healing the differences between a case constant and a case pattern is something we should continue. Just a question, are you proposing that case Point(0, 0) -> ... is semantically equivalent to case Point(var x, var y) when x == 0 && y == 0 -> ... or case Point(int x, int y) when x == 0 && y == 0 -> ... // because 0 is an int I suppose it's the former and in that case, i agree, if it's the later, i don't :) > > We have other new Amber features in the pipeline, but we propose to prioritize > these two features, along with the primitive patterns feature already in > preview. Please look out for the announcements of draft JEPs when they arrive - > as always, we value enormously your help in designing new features for our > favorite programming language! > > Wishing you a happy and successful 2026! > Gavin regards, R?mi From amaembo at gmail.com Wed Jan 14 18:49:38 2026 From: amaembo at gmail.com (Tagir Valeev) Date: Wed, 14 Jan 2026 19:49:38 +0100 Subject: Amber features 2026 In-Reply-To: <474538796.15268341.1768414646795.JavaMail.zimbra@univ-eiffel.fr> References: <1C8344A8-B712-422D-9F66-FB4ED89373FC@oracle.com> <474538796.15268341.1768414646795.JavaMail.zimbra@univ-eiffel.fr> Message-ID: Hi, Remi! Not mentioning the type name at the use site will greatly reduce IDE performance for find usages functionality. Imagine, one wants to find the uses of a particular record component. Now, we can use identifier index to quickly find all the files that mention the record name, and perform more detailed search on this much smaller subset of files. In your proposed syntax, (var a, var b) = foo().bar() is completely opaque. To understand which record is used here, we need to perform full resolve and type inference for foo() and bar(), which may depend on content of other source files, so we cannot use only content of the current file as a cache dependency. We already have a huge problem when searching for functional interface implementations (i.e. all the lambdas and method references that implement it). The functional interface is usually not mentioned explicitly near the lambda, so such a search is extremely slow on big projects. With best regards, Tagir Valeev On Wed, Jan 14, 2026, 19:17 Remi Forax wrote: > ----- Original Message ----- > > From: "Gavin Bierman" > > To: "amber-spec-experts" > > Sent: Saturday, January 10, 2026 12:08:05 AM > > Subject: Amber features 2026 > > > Dear spec experts, > > > > Happy New Year to you all! We thought this was a good time to share you > some of > > the thinking regarding Amber features for 2026. > > > > Currently we have one feature in preview - Primitive Patterns. We?d love > to get > > more feedback on this feature - please keep kicking the tires! > > > > We plan two new features in the near term. Draft JEPs are being worked > on and > > will be released as soon as possible. But here are some brief details > while you > > are waiting for the draft JEPs (in the name of efficiency, *please* > let's save > > discussion for that point). > > > > ## PATTERN ASSIGNMENT > > > > Pattern matching is an inherently partial process: a value either > matches a > > pattern, or it does not. But sometimes, we know that the pattern will > always > > match; and we are using the pattern matching process as a convenient > means to > > disassemble a value, for example: > > > > record ColorPoint(int x, int y, RGB color) {} > > > > void somethingImportant(ColorPoint cp) { > > if (cp instanceof ColorPoint(var x, var y, var c)) { > > // important code > > } > > } > > > > The use of pattern matching is great, but the fact that we have to use > it in a > > conditional statement is annoying. It?s clutter, and worse, it is making > > something known by the developer and compiler look as if it were > unknown; and, > > as a consequence, the important code ends up being indented and the > scope of > > the pattern variables is limited to the then block. The indent-adverse > > developer may reach for the following, but it?s hardly better: > > > > void somethingImportant(ColorPoint cp) { > > if (!(cp instanceof ColorPoint(var x, var y, var c))) { > > return; > > } > > // important code > > } > > > > The real issue here is that both the developer and the compiler can see > that the > > pattern matching is not partial - it will always succeed - but we have > no way > > of recording this semantic information. > > > > What we really want is a form of assignment where the left-hand-side is > not a > > variable but a **pattern**. So, we can rewrite our method as follows: > > > > void somethingImportant(ColorPoint cp) { > > ColorPoint(var x, var y, var c) = cp; // Pattern Assignment! > > // important code > > } > > > > Luckily, the spec already defines what it means for a pattern to be > > unconditional (JLS 14.30.3), so we can build on this > > > > void hopeful(Object o) { > > ColorPoint(var x, var y, var c) = o; // Compile-time error! > > } > > > > > > Doing the advent of code of last December, I miss that feature :) > > But I'm still ambivalent about that feature, for me, it looks like we are > missing the big picture. > > > Every time i've talked about this feature in JUGs, one of the questions > was why do we need to indicate the type given that the compiler knows it. > For example > > void hopeful(ColorPoint cp) { > (var x, var y, var c) = cp; > > // instead of > > ColorPoint(var x, var y, var c) = cp; > } > > > I wonder if the general question hidden behind is why is it a pattern > assignment and not a de-structuration like in other languages. > > Let's take another example, in other languages, one can write swap like > this > int a = ... > int b = ... > (b, a) = (a, b); > > The equivalent would be > > record Pair(int first, int second) {} > int a = ... > int b = ... > Pair(var x, var y) = new Pair(a, b); > a = x; > b = y; > > Or should we support assignment without the creation of new bindings ? > > record Pair(int first, int second) {} > int a = ... > int b = ... > Pair(b, a) = new Pair(a, b); > > Or maybe, this feature should be named pattern declaration and not pattern > assignment ? > > > For me, this feature is about de-structuring assignment but by seeing > through the keyhole of patterns, i'm fearing we are missing the big picture > here. > > regards, > R?mi > -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Wed Jan 14 19:50:36 2026 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Wed, 14 Jan 2026 20:50:36 +0100 (CET) Subject: Amber features 2026 In-Reply-To: References: <1C8344A8-B712-422D-9F66-FB4ED89373FC@oracle.com> <474538796.15268341.1768414646795.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <1947550825.15427060.1768420236830.JavaMail.zimbra@univ-eiffel.fr> > From: "Tagir Valeev" > To: "Remi Forax" > Cc: "Gavin Bierman" , "amber-spec-experts" > > Sent: Wednesday, January 14, 2026 7:49:38 PM > Subject: Re: Amber features 2026 > Hi, Remi! Hello Tagir, > Not mentioning the type name at the use site will greatly reduce IDE performance > for find usages functionality. Imagine, one wants to find the uses of a > particular record component. Now, we can use identifier index to quickly find > all the files that mention the record name, and perform more detailed search on > this much smaller subset of files. In your proposed syntax, (var a, var b) = > foo().bar() is completely opaque. To understand which record is used here, we > need to perform full resolve and type inference for foo() and bar(), which may > depend on content of other source files, so we cannot use only content of the > current file as a cache dependency. Is it different from var a = foo().bar().getA(); var b = foo().bar().getB(); which works like a charm in IntelliJ. That said, I curently do not use 'var' in record patterns because IDEs (not IntelliJ) tend to miss those type references. > We already have a huge problem when searching for functional interface > implementations (i.e. all the lambdas and method references that implement it). > The functional interface is usually not mentioned explicitly near the lambda, > so such a search is extremely slow on big projects. In IntelliJ, you can add a lambda symbol in the gutter part of the editor, it will give you the functional interface. So yes, relying on type inference makes compilers slower and the IDEs slower, and reconciling indexes is a PITA, but suppressing redundant information in the code, for example (var x, var y) = foo.getPoint(); also helps readability (see [ https://openjdk.org/projects/amber/guides/lvti-style-guide | https://openjdk.org/projects/amber/guides/lvti-style-guide ] ) > With best regards, > Tagir Valeev regards, R?mi > On Wed, Jan 14, 2026, 19:17 Remi Forax < [ mailto:forax at univ-mlv.fr | > forax at univ-mlv.fr ] > wrote: >> ----- Original Message ----- >>> From: "Gavin Bierman" < [ mailto:gavin.bierman at oracle.com | >> > gavin.bierman at oracle.com ] > >>> To: "amber-spec-experts" < [ mailto:amber-spec-experts at openjdk.java.net | >> > amber-spec-experts at openjdk.java.net ] > >> > Sent: Saturday, January 10, 2026 12:08:05 AM >> > Subject: Amber features 2026 >> > Dear spec experts, >> > Happy New Year to you all! We thought this was a good time to share you some of >> > the thinking regarding Amber features for 2026. >> > Currently we have one feature in preview - Primitive Patterns. We?d love to get >> > more feedback on this feature - please keep kicking the tires! >> > We plan two new features in the near term. Draft JEPs are being worked on and >> > will be released as soon as possible. But here are some brief details while you >> > are waiting for the draft JEPs (in the name of efficiency, *please* let's save >> > discussion for that point). >> > ## PATTERN ASSIGNMENT >> > Pattern matching is an inherently partial process: a value either matches a >> > pattern, or it does not. But sometimes, we know that the pattern will always >> > match; and we are using the pattern matching process as a convenient means to >> > disassemble a value, for example: >> > record ColorPoint(int x, int y, RGB color) {} >> > void somethingImportant(ColorPoint cp) { >> > if (cp instanceof ColorPoint(var x, var y, var c)) { >> > // important code >> > } >> > } >> > The use of pattern matching is great, but the fact that we have to use it in a >> > conditional statement is annoying. It?s clutter, and worse, it is making >> > something known by the developer and compiler look as if it were unknown; and, >> > as a consequence, the important code ends up being indented and the scope of >> > the pattern variables is limited to the then block. The indent-adverse >> > developer may reach for the following, but it?s hardly better: >> > void somethingImportant(ColorPoint cp) { >> > if (!(cp instanceof ColorPoint(var x, var y, var c))) { >> > return; >> > } >> > // important code >> > } >> > The real issue here is that both the developer and the compiler can see that the >> > pattern matching is not partial - it will always succeed - but we have no way >> > of recording this semantic information. >> > What we really want is a form of assignment where the left-hand-side is not a >> > variable but a **pattern**. So, we can rewrite our method as follows: >> > void somethingImportant(ColorPoint cp) { >> > ColorPoint(var x, var y, var c) = cp; // Pattern Assignment! >> > // important code >> > } >> > Luckily, the spec already defines what it means for a pattern to be >> > unconditional (JLS 14.30.3), so we can build on this >> > void hopeful(Object o) { >> > ColorPoint(var x, var y, var c) = o; // Compile-time error! >> > } >> Doing the advent of code of last December, I miss that feature :) >> But I'm still ambivalent about that feature, for me, it looks like we are >> missing the big picture. >> Every time i've talked about this feature in JUGs, one of the questions was why >> do we need to indicate the type given that the compiler knows it. >> For example >> void hopeful(ColorPoint cp) { >> (var x, var y, var c) = cp; >> // instead of >> ColorPoint(var x, var y, var c) = cp; >> } >> I wonder if the general question hidden behind is why is it a pattern assignment >> and not a de-structuration like in other languages. >> Let's take another example, in other languages, one can write swap like this >> int a = ... >> int b = ... >> (b, a) = (a, b); >> The equivalent would be >> record Pair(int first, int second) {} >> int a = ... >> int b = ... >> Pair(var x, var y) = new Pair(a, b); >> a = x; >> b = y; >> Or should we support assignment without the creation of new bindings ? >> record Pair(int first, int second) {} >> int a = ... >> int b = ... >> Pair(b, a) = new Pair(a, b); >> Or maybe, this feature should be named pattern declaration and not pattern >> assignment ? >> For me, this feature is about de-structuring assignment but by seeing through >> the keyhole of patterns, i'm fearing we are missing the big picture here. >> regards, >> R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Jan 14 20:05:04 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 14 Jan 2026 15:05:04 -0500 Subject: Amber features 2026 In-Reply-To: <474538796.15268341.1768414646795.JavaMail.zimbra@univ-eiffel.fr> References: <1C8344A8-B712-422D-9F66-FB4ED89373FC@oracle.com> <474538796.15268341.1768414646795.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <76e2f04e-d4ee-4359-a3ac-3ff1bfb0c64e@oracle.com> > But I'm still ambivalent about that feature, for me, it looks like we are missing the big picture. > > > Every time i've talked about this feature in JUGs, one of the questions was why do we need to indicate the type given that the compiler knows it. Except that this is not big picture; it's more like code-golf. Yes, the part of people's brains that, when shown a new feature, they will try to squeeze out tokens that they do not see as essential, works in permanent overdrive, so OF COURSE people will ask this.? But this is surface, not substance; the substance is that "exhaustive pattern" is a thing, and it behaves very very similiarly to existing rules about assignment. There are future ideas in the pipeline that might be layered on and reduce the ceremony of this and other things, but we're going to leave that for another day. > I wonder if the general question hidden behind is why is it a pattern assignment and not a de-structuration like in other languages. Because Java does not have destructuring, because it does not have structural types.? It has records (nominal types) and record patterns (and eventually carriers and deconstruction patterns for them), but patterns are a step removed from destructuring. > Or maybe, this feature should be named pattern declaration and not pattern assignment ? The key aspect here is that we have a pattern that is known to be exhaustive, from which we want to extract (assign) multiple bindings unconditionally.? Currently, such a pattern would have to use a conditional construct, which (a) requires you use conditionality to deal with something unconditional, and (b) prevents the compiler from doing better type checking.? When a pattern is exhaustive, it behaves _just like local variable declaration with initialization_, which operates by assignment.? You can think of "pattern assignment" as a shorthand for for that. > For me, this feature is about de-structuring assignment but by seeing through the keyhole of patterns, i'm fearing we are missing the big picture here. Please show me what you think is the big picture, because these examples all look like code golf to me so far. From brian.goetz at oracle.com Wed Jan 14 20:30:56 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 14 Jan 2026 15:30:56 -0500 Subject: Amber features 2026 In-Reply-To: <474538796.15268341.1768414646795.JavaMail.zimbra@univ-eiffel.fr> References: <1C8344A8-B712-422D-9F66-FB4ED89373FC@oracle.com> <474538796.15268341.1768414646795.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <33abc55c-8542-43ab-9320-f0c046ca252b@oracle.com> > Or should we support assignment without the creation of new bindings ? > > record Pair(int first, int second) {} > int a = ... > int b = ... > Pair(b, a) = new Pair(a, b); This one we answered pretty comprehensively much earlier in the pattern matching arc: no. From forax at univ-mlv.fr Wed Jan 14 20:37:09 2026 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Wed, 14 Jan 2026 21:37:09 +0100 (CET) Subject: Amber features 2026 In-Reply-To: <76e2f04e-d4ee-4359-a3ac-3ff1bfb0c64e@oracle.com> References: <1C8344A8-B712-422D-9F66-FB4ED89373FC@oracle.com> <474538796.15268341.1768414646795.JavaMail.zimbra@univ-eiffel.fr> <76e2f04e-d4ee-4359-a3ac-3ff1bfb0c64e@oracle.com> Message-ID: <1566680486.15451404.1768423029638.JavaMail.zimbra@univ-eiffel.fr> ----- Original Message ----- > From: "Brian Goetz" > To: "Remi Forax" , "Gavin Bierman" > Cc: "amber-spec-experts" > Sent: Wednesday, January 14, 2026 9:05:04 PM > Subject: Re: Amber features 2026 >> But I'm still ambivalent about that feature, for me, it looks like we are >> missing the big picture. >> >> >> Every time i've talked about this feature in JUGs, one of the questions was why >> do we need to indicate the type given that the compiler knows it. > > Except that this is not big picture; it's more like code-golf. > > Yes, the part of people's brains that, when shown a new feature, they > will try to squeeze out tokens that they do not see as essential, works > in permanent overdrive, so OF COURSE people will ask this.? But this is > surface, not substance; the substance is that "exhaustive pattern" is a > thing, and it behaves very very similiarly to existing rules about > assignment. > > There are future ideas in the pipeline that might be layered on and > reduce the ceremony of this and other things, but we're going to leave > that for another day. Agree on the fact that exhaustive pattern is a thing. If we rewind a bit, for a case inside a switch, it makes sense to creates new bindings because re-using local variables does not make a lot of sense, we want people to use the fact that a switch can yield a value for sending values between the switch and the rest of the program. For an instanceof, again we want bindings so there is no need to write a cast just after the instanceof. Now for an assignment, this is far less clear for me, that we do not want users to be able to choose if they want fresh new local variables or reuse existing ones (i do not think that mixing both should be supported). By piggybacking on patterns, we are restricting ourselves to think in terms of patterns and not more broadly in terms of de-structuring/multiple assignments. > >> I wonder if the general question hidden behind is why is it a pattern assignment >> and not a de-structuration like in other languages. > Because Java does not have destructuring, because it does not have > structural types.? It has records (nominal types) and record patterns > (and eventually carriers and deconstruction patterns for them), but > patterns are a step removed from destructuring. > >> Or maybe, this feature should be named pattern declaration and not pattern >> assignment ? > > The key aspect here is that we have a pattern that is known to be > exhaustive, from which we want to extract (assign) multiple bindings > unconditionally.? Currently, such a pattern would have to use a > conditional construct, which (a) requires you use conditionality to deal > with something unconditional, and (b) prevents the compiler from doing > better type checking.? When a pattern is exhaustive, it behaves _just > like local variable declaration with initialization_, which operates by > assignment.? You can think of "pattern assignment" as a shorthand for > for that. Asking the question differently, apart from the fact that that this feature has a name that starts with "pattern", do you think that re-assigning local variables is something that should be considered for this feature ? regards, R?mi From brian.goetz at oracle.com Wed Jan 14 20:47:15 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 14 Jan 2026 15:47:15 -0500 Subject: Amber features 2026 In-Reply-To: <1566680486.15451404.1768423029638.JavaMail.zimbra@univ-eiffel.fr> References: <1C8344A8-B712-422D-9F66-FB4ED89373FC@oracle.com> <474538796.15268341.1768414646795.JavaMail.zimbra@univ-eiffel.fr> <76e2f04e-d4ee-4359-a3ac-3ff1bfb0c64e@oracle.com> <1566680486.15451404.1768423029638.JavaMail.zimbra@univ-eiffel.fr> Message-ID: > > If we rewind a bit, for a case inside a switch, it makes sense to creates new bindings ... > > For an instanceof, again we want bindings ... > > Now for an assignment, this is far less clear for me, that we do not want users to be able to choose if they want fresh new local variables or reuse existing ones (i do not think that mixing both should be supported). I think you are over-indexing on the shorthand name given to this feature. When we write ? ? String s = f() we colloquially think of this as "assignment", but technically the name of this is "local variable declaration with initializer" (LVDI). And, look closely at this LVDI usage: is it _really_ a LVDI, or is it a type pattern being unconditionally matched to the expression `f()`?? The answer is: IT DOESN'T MATTER!? The two have exactly the same semantics.? This is not an accident; exhaustive patterns are a strict generalization of LVDI.? (There are some minor corner cases that can be addressed with small cleanups in JLS Ch5.) We could find a more accurate name than "Pattern Assignment", but my worry is that this would obfuscate the very real similarity between LVDI and pattern assignment. And we have definitely discussed and ruled out "use any old mutable variable as a nested pattern", and I don't see any reason to reopen that question just because the word "assignment" is being used here. > By piggybacking on patterns, we are restricting ourselves to think in terms of patterns and not more broadly in terms of de-structuring/multiple assignments. I think you have this backwards.? Patterns, specifically record/deconstruction patterns, _are_ Java's notion of destructuring.? Just as records are our tuples, and functional interfaces are our function types.? Just as we are not adding tuples or structural function types, it would be a self-inflicted wound to add some sort of destructuring that does not run through pattern matching. > Asking the question differently, apart from the fact that that this feature has a name that starts with "pattern", > do you think that re-assigning local variables is something that should be considered for this feature ? I have two answers to that question. 1.? It makes no sense to think of this as part of _this feature_, because this feature is built on pattern matching.? If you want that, then you are asking for it as part of pattern matching in general, where you can turn a mutable variable into a pattern somehow. 2.? I think we very much *do not want* pattern matching to be able to mutate existing variables, for multiple reasons.? This was already well covered the first time around. Short answer; absolutely not. From amaembo at gmail.com Wed Jan 14 21:05:21 2026 From: amaembo at gmail.com (Tagir Valeev) Date: Wed, 14 Jan 2026 22:05:21 +0100 Subject: Amber features 2026 In-Reply-To: <1947550825.15427060.1768420236830.JavaMail.zimbra@univ-eiffel.fr> References: <1C8344A8-B712-422D-9F66-FB4ED89373FC@oracle.com> <474538796.15268341.1768414646795.JavaMail.zimbra@univ-eiffel.fr> <1947550825.15427060.1768420236830.JavaMail.zimbra@univ-eiffel.fr> Message-ID: Hi! On Wed, Jan 14, 2026, 20:50 wrote > Is it different from > var a = foo().bar().getA(); > var b = foo().bar().getB(); > > which works like a charm in IntelliJ. > Here, if you search for getA invocation, you can query the identifier index for 'getA', which is mentioned. If you want to search for the type of a, then this location will not be found, because the type is not mentioned. Find usages is crucial for refactorings. If you rename the type, you don't need to find this location, because the type is not mentioned. However, if you reorder record components, you have to find all the deconstructions to update them as well, even if the record is not mentioned. Similarly, if you change the signature of functional interface SAM, you have to update all the lambdas, so you have to find them. > That said, I curently do not use 'var' in record patterns because IDEs > (not IntelliJ) tend to miss those type references. > > In IntelliJ, you can add a lambda symbol in the gutter part of the editor, > it will give you the functional interface. > Going from lambda to its functional interface is much simpler than the opposite task of finding all the lambdas in the project that implement this interface. While it's still a nontrivial type inference problem, it should be performed at a single location, not at thousands of possible locations all over the project. > So yes, relying on type inference makes compilers slower and the IDEs > slower, and reconciling indexes is a PITA, > but suppressing redundant information in the code, for example > (var x, var y) = foo.getPoint(); > > also helps readability (see > https://openjdk.org/projects/amber/guides/lvti-style-guide) > > > With best regards, > Tagir Valeev > > > regards, > R?mi > > > On Wed, Jan 14, 2026, 19:17 Remi Forax wrote: > >> ----- Original Message ----- >> > From: "Gavin Bierman" >> > To: "amber-spec-experts" >> > Sent: Saturday, January 10, 2026 12:08:05 AM >> > Subject: Amber features 2026 >> >> > Dear spec experts, >> > >> > Happy New Year to you all! We thought this was a good time to share you >> some of >> > the thinking regarding Amber features for 2026. >> > >> > Currently we have one feature in preview - Primitive Patterns. We?d >> love to get >> > more feedback on this feature - please keep kicking the tires! >> > >> > We plan two new features in the near term. Draft JEPs are being worked >> on and >> > will be released as soon as possible. But here are some brief details >> while you >> > are waiting for the draft JEPs (in the name of efficiency, *please* >> let's save >> > discussion for that point). >> > >> > ## PATTERN ASSIGNMENT >> > >> > Pattern matching is an inherently partial process: a value either >> matches a >> > pattern, or it does not. But sometimes, we know that the pattern will >> always >> > match; and we are using the pattern matching process as a convenient >> means to >> > disassemble a value, for example: >> > >> > record ColorPoint(int x, int y, RGB color) {} >> > >> > void somethingImportant(ColorPoint cp) { >> > if (cp instanceof ColorPoint(var x, var y, var c)) { >> > // important code >> > } >> > } >> > >> > The use of pattern matching is great, but the fact that we have to use >> it in a >> > conditional statement is annoying. It?s clutter, and worse, it is making >> > something known by the developer and compiler look as if it were >> unknown; and, >> > as a consequence, the important code ends up being indented and the >> scope of >> > the pattern variables is limited to the then block. The indent-adverse >> > developer may reach for the following, but it?s hardly better: >> > >> > void somethingImportant(ColorPoint cp) { >> > if (!(cp instanceof ColorPoint(var x, var y, var c))) { >> > return; >> > } >> > // important code >> > } >> > >> > The real issue here is that both the developer and the compiler can see >> that the >> > pattern matching is not partial - it will always succeed - but we have >> no way >> > of recording this semantic information. >> > >> > What we really want is a form of assignment where the left-hand-side is >> not a >> > variable but a **pattern**. So, we can rewrite our method as follows: >> > >> > void somethingImportant(ColorPoint cp) { >> > ColorPoint(var x, var y, var c) = cp; // Pattern Assignment! >> > // important code >> > } >> > >> > Luckily, the spec already defines what it means for a pattern to be >> > unconditional (JLS 14.30.3), so we can build on this >> > >> > void hopeful(Object o) { >> > ColorPoint(var x, var y, var c) = o; // Compile-time error! >> > } >> > >> > >> >> Doing the advent of code of last December, I miss that feature :) >> >> But I'm still ambivalent about that feature, for me, it looks like we are >> missing the big picture. >> >> >> Every time i've talked about this feature in JUGs, one of the questions >> was why do we need to indicate the type given that the compiler knows it. >> For example >> >> void hopeful(ColorPoint cp) { >> (var x, var y, var c) = cp; >> >> // instead of >> >> ColorPoint(var x, var y, var c) = cp; >> } >> >> >> I wonder if the general question hidden behind is why is it a pattern >> assignment and not a de-structuration like in other languages. >> >> Let's take another example, in other languages, one can write swap like >> this >> int a = ... >> int b = ... >> (b, a) = (a, b); >> >> The equivalent would be >> >> record Pair(int first, int second) {} >> int a = ... >> int b = ... >> Pair(var x, var y) = new Pair(a, b); >> a = x; >> b = y; >> >> Or should we support assignment without the creation of new bindings ? >> >> record Pair(int first, int second) {} >> int a = ... >> int b = ... >> Pair(b, a) = new Pair(a, b); >> >> Or maybe, this feature should be named pattern declaration and not >> pattern assignment ? >> >> >> For me, this feature is about de-structuring assignment but by seeing >> through the keyhole of patterns, i'm fearing we are missing the big picture >> here. >> >> regards, >> R?mi >> > > -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Wed Jan 14 21:31:30 2026 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Wed, 14 Jan 2026 22:31:30 +0100 (CET) Subject: Amber features 2026 In-Reply-To: References: <1C8344A8-B712-422D-9F66-FB4ED89373FC@oracle.com> <474538796.15268341.1768414646795.JavaMail.zimbra@univ-eiffel.fr> <76e2f04e-d4ee-4359-a3ac-3ff1bfb0c64e@oracle.com> <1566680486.15451404.1768423029638.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <2104894209.15509548.1768426290947.JavaMail.zimbra@univ-eiffel.fr> ----- Original Message ----- > From: "Brian Goetz" > To: "Remi Forax" > Cc: "Gavin Bierman" , "amber-spec-experts" > Sent: Wednesday, January 14, 2026 9:47:15 PM > Subject: Re: Amber features 2026 >> >> If we rewind a bit, for a case inside a switch, it makes sense to creates new >> bindings ... >> >> For an instanceof, again we want bindings ... >> >> Now for an assignment, this is far less clear for me, that we do not want users >> to be able to choose if they want fresh new local variables or reuse existing >> ones (i do not think that mixing both should be supported). > > I think you are over-indexing on the shorthand name given to this feature. > > When we write > > ? ? String s = f() > > we colloquially think of this as "assignment", but technically the name > of this is "local variable declaration with initializer" (LVDI). > > And, look closely at this LVDI usage: is it _really_ a LVDI, or is it a > type pattern being unconditionally matched to the expression `f()`?? The > answer is: IT DOESN'T MATTER!? The two have exactly the same semantics. > This is not an accident; exhaustive patterns are a strict generalization > of LVDI.? (There are some minor corner cases that can be addressed with > small cleanups in JLS Ch5.) > > We could find a more accurate name than "Pattern Assignment", but my > worry is that this would obfuscate the very real similarity between LVDI > and pattern assignment. > > And we have definitely discussed and ruled out "use any old mutable > variable as a nested pattern", and I don't see any reason to reopen that > question just because the word "assignment" is being used here. > >> By piggybacking on patterns, we are restricting ourselves to think in terms of >> patterns and not more broadly in terms of de-structuring/multiple assignments. > > I think you have this backwards.? Patterns, specifically > record/deconstruction patterns, _are_ Java's notion of destructuring. > Just as records are our tuples, and functional interfaces are our > function types.? Just as we are not adding tuples or structural function > types, it would be a self-inflicted wound to add some sort of > destructuring that does not run through pattern matching. > >> Asking the question differently, apart from the fact that that this feature has >> a name that starts with "pattern", >> do you think that re-assigning local variables is something that should be >> considered for this feature ? > > I have two answers to that question. > > 1.? It makes no sense to think of this as part of _this feature_, > because this feature is built on pattern matching.? If you want that, > then you are asking for it as part of pattern matching in general, where > you can turn a mutable variable into a pattern somehow. > > 2.? I think we very much *do not want* pattern matching to be able to > mutate existing variables, for multiple reasons.? This was already well > covered the first time around. > > Short answer; absolutely not. Let's try from the other direction, what will make this feature a pain to use if we do not allow to mutate existing variables. So what are the cases in Java where people will want to mutate local variables. - you mutate variables when you reduce accumulators in a loop var v1 = ... var v2 = ... for(...) { (v1, v2) = f(v1, v2); } - you mutate variables after a condition var v1 = ... var v2 = ... if (...) { (v1, v2) = f(...); } - you mutate variables when you transfer values in between scopes Type1 v1; Type2 v2; try(...) { (v1, v2) = f(...); } in all those cases we already know that using pattern assignment will be painful. And the reason of this pain is that this feature is called "pattern assignment" so obviously, because it's a pattern, it can not mutate existing local variables. We are boxing ourselves in a corner by calling this feature pattern assignment, yes, it should use the destructuring part of an exhaustive pattern, but it's not a pattern. regards, R?mi From brian.goetz at oracle.com Wed Jan 14 21:56:40 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 14 Jan 2026 16:56:40 -0500 Subject: Amber features 2026 In-Reply-To: <2104894209.15509548.1768426290947.JavaMail.zimbra@univ-eiffel.fr> References: <1C8344A8-B712-422D-9F66-FB4ED89373FC@oracle.com> <474538796.15268341.1768414646795.JavaMail.zimbra@univ-eiffel.fr> <76e2f04e-d4ee-4359-a3ac-3ff1bfb0c64e@oracle.com> <1566680486.15451404.1768423029638.JavaMail.zimbra@univ-eiffel.fr> <2104894209.15509548.1768426290947.JavaMail.zimbra@univ-eiffel.fr> Message-ID: > We are boxing ourselves in a corner by calling this feature pattern assignment, Pick one: ?- the feature of "imperative application of exhaustive pattern" is a bad feature ?- it's a fine feature, but the name is wrong ?- something else?? (be specific) From forax at univ-mlv.fr Sat Jan 17 09:40:20 2026 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Sat, 17 Jan 2026 10:40:20 +0100 (CET) Subject: Amber features 2026 In-Reply-To: References: <1C8344A8-B712-422D-9F66-FB4ED89373FC@oracle.com> <474538796.15268341.1768414646795.JavaMail.zimbra@univ-eiffel.fr> <1947550825.15427060.1768420236830.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <1211052122.17561472.1768642820104.JavaMail.zimbra@univ-eiffel.fr> Okay, I think i'm starting to understand what you are saying, but correct me if i'm wrong, IntelliJ has several optimizations that try to avoid to go above the syntax level of a compilation unit to the semantics level when doing different refactorings, for that the name of the type as to be explicit. It does not work with streams/lambdas, or anything involving inference so you would prefer to not allow users to use inference in pattern assignment. Now, use cases where omitting the type of the record in an assignment pattern really shine in my opinion is when you have a simple pair or a Map.Entry (I know that Map.Entry is not technically a record but Brian is proposing to go in that direction) and you want to de-structure it. So instead of for((var key, var value) : map.entrySet()) { ... } You would prefer for(Map.Entry(var key, var value) : map.entrySet()) { ... } But for map.entrySet() .map((key, value) -> ...) // maybe add an extra set of parenthesis here ? you do not care here because IntelliJ has already no choice but to do the inference given this is inside a stream. regards, R?mi > From: "Tagir Valeev" > To: "Remi Forax" > Cc: "Gavin Bierman" , "amber-spec-experts" > > Sent: Wednesday, January 14, 2026 10:05:21 PM > Subject: Re: Amber features 2026 > Hi! > On Wed, Jan 14, 2026, 20:50 < [ mailto:forax at univ-mlv.fr | forax at univ-mlv.fr ] > > wrote >> Is it different from >> var a = foo().bar().getA(); >> var b = foo().bar().getB(); >> which works like a charm in IntelliJ. > Here, if you search for getA invocation, you can query the identifier index for > 'getA', which is mentioned. If you want to search for the type of a, then this > location will not be found, because the type is not mentioned. Find usages is > crucial for refactorings. If you rename the type, you don't need to find this > location, because the type is not mentioned. However, if you reorder record > components, you have to find all the deconstructions to update them as well, > even if the record is not mentioned. Similarly, if you change the signature of > functional interface SAM, you have to update all the lambdas, so you have to > find them. >> That said, I curently do not use 'var' in record patterns because IDEs (not >> IntelliJ) tend to miss those type references. >> In IntelliJ, you can add a lambda symbol in the gutter part of the editor, it >> will give you the functional interface. > Going from lambda to its functional interface is much simpler than the opposite > task of finding all the lambdas in the project that implement this interface. > While it's still a nontrivial type inference problem, it should be performed at > a single location, not at thousands of possible locations all over the project. >> So yes, relying on type inference makes compilers slower and the IDEs slower, >> and reconciling indexes is a PITA, >> but suppressing redundant information in the code, for example >> (var x, var y) = foo.getPoint(); >> also helps readability (see [ >> https://openjdk.org/projects/amber/guides/lvti-style-guide | >> https://openjdk.org/projects/amber/guides/lvti-style-guide ] ) >>> With best regards, >>> Tagir Valeev >> regards, >> R?mi >>> On Wed, Jan 14, 2026, 19:17 Remi Forax < [ mailto:forax at univ-mlv.fr | >>> forax at univ-mlv.fr ] > wrote: >>>> ----- Original Message ----- >>>>> From: "Gavin Bierman" < [ mailto:gavin.bierman at oracle.com | >>>> > gavin.bierman at oracle.com ] > >>>>> To: "amber-spec-experts" < [ mailto:amber-spec-experts at openjdk.java.net | >>>> > amber-spec-experts at openjdk.java.net ] > >>>> > Sent: Saturday, January 10, 2026 12:08:05 AM >>>> > Subject: Amber features 2026 >>>> > Dear spec experts, >>>> > Happy New Year to you all! We thought this was a good time to share you some of >>>> > the thinking regarding Amber features for 2026. >>>> > Currently we have one feature in preview - Primitive Patterns. We?d love to get >>>> > more feedback on this feature - please keep kicking the tires! >>>> > We plan two new features in the near term. Draft JEPs are being worked on and >>>> > will be released as soon as possible. But here are some brief details while you >>>> > are waiting for the draft JEPs (in the name of efficiency, *please* let's save >>>> > discussion for that point). >>>> > ## PATTERN ASSIGNMENT >>>> > Pattern matching is an inherently partial process: a value either matches a >>>> > pattern, or it does not. But sometimes, we know that the pattern will always >>>> > match; and we are using the pattern matching process as a convenient means to >>>> > disassemble a value, for example: >>>> > record ColorPoint(int x, int y, RGB color) {} >>>> > void somethingImportant(ColorPoint cp) { >>>> > if (cp instanceof ColorPoint(var x, var y, var c)) { >>>> > // important code >>>> > } >>>> > } >>>> > The use of pattern matching is great, but the fact that we have to use it in a >>>> > conditional statement is annoying. It?s clutter, and worse, it is making >>>> > something known by the developer and compiler look as if it were unknown; and, >>>> > as a consequence, the important code ends up being indented and the scope of >>>> > the pattern variables is limited to the then block. The indent-adverse >>>> > developer may reach for the following, but it?s hardly better: >>>> > void somethingImportant(ColorPoint cp) { >>>> > if (!(cp instanceof ColorPoint(var x, var y, var c))) { >>>> > return; >>>> > } >>>> > // important code >>>> > } >>>> > The real issue here is that both the developer and the compiler can see that the >>>> > pattern matching is not partial - it will always succeed - but we have no way >>>> > of recording this semantic information. >>>> > What we really want is a form of assignment where the left-hand-side is not a >>>> > variable but a **pattern**. So, we can rewrite our method as follows: >>>> > void somethingImportant(ColorPoint cp) { >>>> > ColorPoint(var x, var y, var c) = cp; // Pattern Assignment! >>>> > // important code >>>> > } >>>> > Luckily, the spec already defines what it means for a pattern to be >>>> > unconditional (JLS 14.30.3), so we can build on this >>>> > void hopeful(Object o) { >>>> > ColorPoint(var x, var y, var c) = o; // Compile-time error! >>>> > } >>>> Doing the advent of code of last December, I miss that feature :) >>>> But I'm still ambivalent about that feature, for me, it looks like we are >>>> missing the big picture. >>>> Every time i've talked about this feature in JUGs, one of the questions was why >>>> do we need to indicate the type given that the compiler knows it. >>>> For example >>>> void hopeful(ColorPoint cp) { >>>> (var x, var y, var c) = cp; >>>> // instead of >>>> ColorPoint(var x, var y, var c) = cp; >>>> } >>>> I wonder if the general question hidden behind is why is it a pattern assignment >>>> and not a de-structuration like in other languages. >>>> Let's take another example, in other languages, one can write swap like this >>>> int a = ... >>>> int b = ... >>>> (b, a) = (a, b); >>>> The equivalent would be >>>> record Pair(int first, int second) {} >>>> int a = ... >>>> int b = ... >>>> Pair(var x, var y) = new Pair(a, b); >>>> a = x; >>>> b = y; >>>> Or should we support assignment without the creation of new bindings ? >>>> record Pair(int first, int second) {} >>>> int a = ... >>>> int b = ... >>>> Pair(b, a) = new Pair(a, b); >>>> Or maybe, this feature should be named pattern declaration and not pattern >>>> assignment ? >>>> For me, this feature is about de-structuring assignment but by seeing through >>>> the keyhole of patterns, i'm fearing we are missing the big picture here. >>>> regards, >>>> R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Sat Jan 17 10:36:53 2026 From: forax at univ-mlv.fr (Remi Forax) Date: Sat, 17 Jan 2026 11:36:53 +0100 (CET) Subject: Data Oriented Programming, Beyond Records In-Reply-To: References: Message-ID: <1085557496.17567763.1768646213005.JavaMail.zimbra@univ-eiffel.fr> Very interesting. I see two different goals that i think can be separated (at least it's easier in my head) - one is to extends the notion of record pattern to other types ; enum, class etc ; by allowing "state description" on those types. - the other is to retrofit the record syntax so more classes can benefit for having equals()/hashCode() and toString() (and accessors or de-constructor ?) automatically generated by the compiler. You may think that the first goal is more important that the second one (it's about providing an API) but having the internal implementation derived from the components description limit the chance to have a bad implementation. In my opinion, providing a way to automatically generate equals/hashCode and toString() for a mutable class is just a giant footgun. With a mutable class with equals/hashCode/toString generated, it's too easy to store an object in a collection, mutate it, and then never been able to find it again. For example var mutableObject = new MutableClassWithGeneratedEqualsHashCodeToString(); var set = new HashSet<>(); set.add(mutableObject); set.mutateAComponentField(); set.contains(mutableObject); // false So I disagree that the component can be non final and that the class can be non final (you need those two to be non-modifiable). As Ganapathi Vara Prasad said, maybe you want the reverse semantics, instead of indicating the components, you may want to indicate the non-component fields, the derived fields. But for me, it does not seems useful enough. For me, trying to heal the rift between immutable and mutable class is not recognizing that those two kind of classes does are used very differently and that designing a mutable class requires for more thinking than an immutable one. Now, for the first goal, yes, we should go for that, basically, I like that the syntax reflect the fact that if you have a canonical constructor and de-constructor, you are a special kind of type, what you call a carrier class (BTW, I do not like that term but i do not have a better name to propose). So yes, for example, Map.Entry should be a carrier interface with a default deconstructor public interface Map { interface Entry(K key, V value) {. // this is a carrier interface // to be backward compatible, getKey(), getValue() and setValue() are declared as usual abstract K getKey(); abstract V getValue(); default void setValue(V value) { throw new UOE(); } default Entry deconstructor() { // this method is used to get the bindings when pattern matching return Map.entry(getKey(), getValue()); } } } 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. And a carrier class/interface/enum has to declare a deconstructor, or perhaps if the type is abstract, an abstract deconstructor can be added automatically by the compiler. In case of a record, a concrete deconstructor is added by the compiler. regards, R?mi > From: "Brian Goetz" > To: "amber-spec-experts" > Sent: Tuesday, January 13, 2026 10:52:47 PM > Subject: Data Oriented Programming, Beyond Records > Here's a snapshot of where my head is at with respect to extending the record > goodies (including pattern matching) to a broader range of classes, > deconstructors for classes and interfaces, and compatible evolution of records. > Hopefully this will unblock quite a few things. > As usual, let's discuss concepts and directions rather than syntax. > # Data-oriented Programming for Java: Beyond records > Everyone loves records; they allow us to create shallowly immutable data holder > classes -- which we can think of as "nominal tuples" -- derived from a concise > state description, and to destructure records through pattern matching. But > records have strict constraints, and not all data holder classes fit into the > restrictions of records. Maybe they have some mutable state, or derived or > cached state that is not part of the state description, or their representation > and their API do not match up exactly, or they need to break up their state > across a hierarchy. In these classes, even though they may also be ?data > holders?, the user experience is like falling off a cliff. Even a small > deviation from the record ideal means one has to go back to a blank slate and > write explicit constructor declarations, accessor method declarations, and > Object method implementations -- and give up on destructuring through pattern > matching. > Since the start of the design process for records, we?ve kept in mind the goal > of enabling a broader range of classes to gain access to the "record goodies": > reduced declaration burden, participating in destructuring, and soon, > [reconstruction]( [ https://openjdk.org/jeps/468 | https://openjdk.org/jeps/468 > ] ). During the design of records, we > also explored a number of weaker semantic models that would allow for greater > flexibility. While at the time they all failed to live up to the goals _for > records_, there is a weaker set of semantic constraints we can impose that > allows for more flexibility and still enables the features we want, along with > some degree of syntactic concision that is commensurate with the distance from > the record-ideal, without fall-off-the-cliff behaviors. > Records, sealed classes, and destructuring with record patterns constitute the > first feature arc of "data-oriented programming" for Java. After considering > numerous design ideas, we're now ready to move forward with the next "data > oriented programming" feature arc: _carrier classes_ (and interfaces.) > ## Beyond record patterns > Record patterns allow a record instance to be destructured into its components. > Record patterns can be used in `instanceof` and `switch`, and when a record > pattern is also exhaustive, will be usable in the upcoming [_pattern assignment > statement_]( [ > https://mail.openjdk.org/pipermail/amber-spec-experts/2026-January/004306.html > | > https://mail.openjdk.org/pipermail/amber-spec-experts/2026-January/004306.html > ] ) feature. > In exploring the question "how will classes be able to participate in the same > sort of destructuring as records", we had initially focused on a new form of > declaration in a class -- a "deconstructor" -- that operated as a constructor in > reverse. Just as a constructor takes component values and produces an aggregate > instance, a deconstructor would take an aggregate instance and recover its > component values. > But as this exploration played out, the more interesting question turned out to > be: which classes are suitable for destructuring in the first place? And the > answer to that question led us to a different approach for expressing > deconstruction. The classes that are suitable for destructuring are those that, > like records, are little more than carriers for a specific tuple of data. This > is not just a thing that a class _has_, like a constructor or method, but > something a class _is_. And as such, it makes more sense to describe > deconstruction as a top-level property of a class. This, in turn, leads to a > number of simplifications. > ## The power of the state description > Records are a semantic feature; they are only incidentally concise. But they > _are_ concise; when we declare a record > record Point(int x, int y) { ... } > we automatically get a sensible API (canonical constructor, deconstruction > pattern, accessor methods for each component) and implementation (fields, > constructor, accessor methods, Object methods.) We can explicitly specify most > of these (except the fields) if we like, but most of the time we don't have to, > because the default is exactly what we want. > A record is a shallowly-immutable, final class whose API and representation are > _completely defined_ by its _state description_. (The slogan for records is > "the state, the whole state, and nothing but the state.") The state description > is the ordered list of _record components_ declared in the record's header. A > component is more than a mere field or accessor method; it is an API element on > its own, describing a state element that instances of the class have. > The state description of a record has several desirable properties: > - The components in the order specified, are the _canonical_ description of the > record's state. > - The components are the _complete_ description of the record?s state. > - The components are _nominal_; their names are a committed part of the > record's API. > Records derive their benefits from making two commitments: > - The _external_ commitment that the data-access API of a record (constructor, > deconstruction pattern, and component accessor methods) is defined by the > state description. > - The _internal_ commitments that the _representation_ of the record (its > fields) is also completely defined by the state description. > These semantic properties are what enable us to derive almost everything about > records. We can derive the API of the canonical constructor because the state > description is canonical. We can derive the API for the component accessor > methods because the state description is nominal. And we can derive a > deconstruction pattern from the accessor methods because the state description > is complete (along with sensible implementations for the state-related `Object` > methods.) > The internal commitment that the state description is also the representation > allows us to completely derive the rest of the implementation. Records get a > (private, final) field for each component, but more importantly, there is a > clear mapping between these fields and their corresponding components, which is > what allows us to derive the canonical constructor and accessor method > implementations. > Records can additionally declare a _compact constructor_ that allows us to elide > the boilerplate aspects of record constructors -- the argument list and field > assignments -- and just specify the code that is _not_ mechanically derivable. > This is more concise, less error-prone, and easier to read: > record Rational(int num, int denom) { > Rational { > if (denom == 0) > throw new IllegalArgumentException("denominator cannot be zero"); > } > } > is shorthand for the more explicit > record Rational(int num, int denom) { > Rational(int num, int denom) { > if (denom == 0) > throw new IllegalArgumentException("denominator cannot be zero"); > this.num = num; > this.denom = denom; > } > } > While compact constructors are pleasantly concise, the more important benefit is > that by eliminating the mechanically derivable code, the "more interesting" code > comes to the fore. > Looking ahead, the state description is a gift that keeps on giving. These > semantic commitments are enablers for a number of potential future language and > library features for managing object lifecycle, such as: > - [Reconstruction]( [ https://openjdk.org/jeps/468 | > https://openjdk.org/jeps/468 ] ) of record instances, allowing > the appearance of controlled mutation of record state. > - Automatic marshalling and unmarshalling of record instances. > - Instantiating or destructuring record instances identifying components > nominally rather than positionally. > ### Reconstruction > JEP 468 proposes a mechanism by which a new record instance can be derived from > an existing one using syntax that is evocative of direct mutation, via a `with` > expression: > record Complex(double re, double im) { } > Complex c = ... > Complex cConjugate = c with { im = -im; }; > The block on the right side of `with` can contain any Java statements, not just > assignments. It is enhanced with mutable variables (_component variables_) for > each component of the record, initialized to the value of that component in the > record instance on the left, the block is executed, and a new record instance is > created whose component values are the ending values of the component variables. > A reconstruction expression implicitly destructures the record instance using > the canonical deconstruction pattern, executes the block in a scope enhanced > with the component variables, and then creates a new record using the canonical > constructor. Invariant checking is centralized in the canonical constructor, so > if the new state is not valid, the reconstruction will fail. JEP 468 has been > "on hold" for a while, primarily because we were waiting for sufficient > confidence that there was a path to extending it to suitable classes before > committing to it for records. The ideal path would be for those classes to also > support a notion of canonical constructor and deconstruction pattern. > Careful readers will note a similarity between the transformation block of a > `with` expression and the body of a compact constructor. In both cases, the > block is "preloaded" with a set of component variables, initialized to suitable > starting values, the block can mutate those variables as desired, and upon > normal completion of the block, those variables are passed to a canonical > constructor to produce the final result. The main difference is where the > starting values come from; for a compact constructor, it is from the constructor > parameters, and for a reconstruction expression, it is from the canonical > deconstruction pattern of the source record to the left of `with`. > ### Breaking down the cliff > Records make a strong semantic commitment to derive both their API and > representation from the state description, and in return get a lot of help from > the language. We can now turn our attention to smoothing out "the cliff" -- > identifying weaker semantic commitments that classes can make that would still > allow classes to get _some_ help from the language. And ideally, the amount of > help you give up would be proportional to the degree of deviation from the > record ideal. > With records, we got a lot of mileage out of having a complete, canonical, > nominal state description. Where the record contract is sometimes too > constraining is the _implementation_ contract that the representation aligns > exactly with the state description, that the class is final, that the fields are > final, and that the class may not extend anything but `Record`. > Our path here takes one step back and one step forward: keeping the external > commitment to the state description, but dropping the internal commitment that > the state description _is_ the representation -- and then _adding back_ a simple > mechanism for mapping fields representing components back to their corresponding > components, where practical. (With records, because we derive the > representation from the state description, this mapping can be safely inferred.) > As a thought experiment, imagine a class that makes the external commitment to a > state description -- that the state description is a complete, canonical, > nominal description of its state -- but is on its own to provide its > representation. What can we do for such a class? Quite a bit, actually. For > all the same reasons we can for records, we can derive the API requirement for a > canonical constructor and component accessor methods. From there, we can derive > both the requirement for a canonical deconstruction pattern, and also the > implementation of the deconstruction pattern (as it is implemented in terms of > the accessor methods). And since the state description is complete, we can > further derive sensible default implementations of the Object methods `equals`, > `hashCode`, and `toString` in terms of the accessor methods as well. And given > that there is a canonical constructor and deconstruction pattern, it can also > participate in reconstruction. The author would just have to provide the > fields, accessor methods, and canonical constructor. This is good progress, but > we'd like to do better. > What enables us to derive the rest of the implementation for records (fields, > constructor, accessor methods, and Object methods) is the knowledge of how the > representation maps to the state description. Records commit to their state > description _being_ the representation, so is is a short leap from there to a > complete implementation. > To make this more concrete, let's look at a typical "almost record" class, a > carrier for the state description `(int x, int y, Optional s)` but which > has made the representation choice to internally store `s` as a nullable > `String`. > ``` > class AlmostRecord { > private final int x; > private final int y; > private final String s; // * > public AlmostRecord(int x, int y, Optional s) { > this.x = x; > this.y = y; > this.s = s.orElse(null); // * > } > public int x() { return x; } > public int y() { return y; } > public Optional s() { > return Optional.ofNullable(s); // * > } > public boolean equals(Object other) { ... } // derived from x(), y(), s() > public int hashCode() { ... } // " > public String toString() { ... } // " > } > ``` > The main differences between this class and the expansion of its record analogue > are the lines marked with a `*`; these are the ones that deal with the disparity > between the state description and the actual representation. It would be nice > if the author of this class _only_ had to write the code that was different from > what we could derive for a record; not only would this be pleasantly concise, > but it would mean that all the code that _is_ there exists to capture the > differences between its representation and its API. > ## Carrier classes > A _carrier class_ is a normal class declared with a state description. As with > a record, the state description is a complete, canonical, nominal description of > the class's state. In return, the language derives the same API constraints as > it does for records: canonical constructor, canonical deconstruction pattern, > and component accessor methods. > class Point(int x, int y) { // class, not record! > // explicitly declared representation > ... > // must have a constructor taking (int x, int y) > // must have accessors for x and y > // supports a deconstruction pattern yielding (int x, int y) > } > Unlike a record, the language makes no assumptions about the object's > representation; the class author has to declare that just as with any other > class. > Saying the state description is "complete" means that it carries all the > ?important? state of the class -- if we were to extract this state and recreate > the object, that should yield an ?equivalent? instance. As with records, this > can be captured by tying together the behavior of construction, accessors, and > equality: > ``` > Point p = ... > Point q = new Point(p.x(), p.y()); > assert p.equals(q); > ``` > We can also derive _some_ implementation from the information we have so far; we > can derive sensible implementations of the `Object` methods (implemented in > terms > of component accessor methods) and we can derive the canonical deconstruction > pattern (again in terms of the component accessor methods). And from there, we > can derive support for reconstruction (`with` expressions.) Unfortunately, we > cannot (yet) derive the bulk of the state-related implementation: the canonical > constructor and component accessor methods. > ### Component fields and accessor methods > One of the most tedious aspects of data-holder classes is the accessor methods; > there are often many of them, and they are almost always pure boilerplate. Even > though IDEs can reduce the writing burden by generating these for us, readers > still have to slog through a lot of low-information code -- just to learn that > they didn't actually need to slog through that code after all. We can derive > the implementation of accessor methods for records because records make the > internal commitment that the components are all backed with individual fields > whose name and type align with the state description. > For a carrier class, we don't know whether _any_ of the components are directly > backed by a single field that aligns to the name or type of the component. But > it is a pretty good bet that many carrier class components will do exactly this > for at least _some_ of their fields. If we can tell the language that this > correspondence is not merely accidental, the language can do more for us. > We do so by allowing suitable fields of a carrier class to be declared as > `component` fields. (As usual at this stage, syntax is provisional, but not > currently a topic for discussion.) A component field must have the same name > and type as a component of the current class (though it need not be `private` or > `final`, as record fields are.) This signals that this field _is_ the > representation for the corresponding component, and hence we can derive the > accessor method for this component as well. > ``` > class Point(int x, int y) { > private /* mutable */ component int x; > private /* mutable */ component int y; > // must have a canonical constructor, but (so far) must be explicit > public Point(int x, int y) { > this.x = x; > this.y = y; > } > // derived implementations of accessors for x and y > // derived implementations of equals, hashCode, toString > } > ``` > This is getting better; the class author had to bring the representation and the > mapping from representation to components (in the form of the `component` > modifier), and the canonical constructor. > ### Compact constructors > Just as we are able to derive the accessor method implementation if we are > given an explicit correspondence between a field and a component, we can do the > same for constructors. For this, we build on the notion of _compact > constructors_ that was introduced for records. > As with a record, a compact constructor in a carrier class is a shorthand for a > canonical constructor, which has the same shape as the state description, but > which is freed of the responsibility of actually committing the ending value of > the component parameters to the fields. The main difference is that for a > record, _all_ of the components are backed by a component field, whereas for a > carrier class, only some of them might be. But we can generalize compact > constructors by freeing the author of the responsibility to initialize the > _component_ fields, while leaving them responsible for initializing the rest of > the fields. In the limiting case where all components are backed by component > fields, and there is no other logic desired in the constructor, the compact > constructor may be elided. > For our mutable `Point` class, this means we can elide nearly everything, except > the field declarations themselves: > ``` > class Point(int x, int y) { > private /* mutable */ component int x; > private /* mutable */ component int y; > // derived compact constructor > // derived accessors for x, y > // derived implementations of equals, hashCode, toString > } > ``` > We can think of this class as having an implicit empty compact constructor, > which in turn means that the component fields `x` and `y` are initialized from > their corresponding constructor parameters. There are also implicitly derived > accessor methods for each component, and implementations of `Object` methods > based on the state description. > This is great for a class where all the components are backed by fields, but > what about our `AlmostRecord` class? The story here is good as well; we can > derive the accessor methods for the components backed by component fields, and > we can elide the initialization of the component fields from the compact > constructor, meaning that we _only_ have to specify the code for the parts that > deviate from the "record ideal": > ``` > class AlmostRecord(int x, > int y, > Optional s) { > private final component int x; > private final component int y; > private final String s; > public AlmostRecord { > this.s = s.orElse(null); > // x and y fields implicitly initialized > } > public Optional s() { > return Optional.ofNullable(s); > } > // derived implementation of x and y accessors > // derived implementation of equals, hashCode, toString > } > ``` > Because so many real-world almost-records differ from their record ideal in > minor ways, we expect to get a significant concision benefit for most carrier > classes, as we did for `AlmostRecord`. As with records, if we want to > explicitly implement the constructor, accessor methods, or `Object` methods, we > are still free to do so. > ### Derived state > One of the most frequent complaints about records is the inability to derive > state from the components and cache it for fast retrieval. With carrier > classes, this is simple: declare a non-component field for the derived quantity, > initialize it in the constructor, and provide an accessor: > ``` > class Point(int x, int y) { > private final component int x; > private final component int y; > private final double norm; > Point { > norm = Math.hypot(x, y); > } > public double norm() { return norm; } > // derived implementation of x and y accessors > // derived implementation of equals, hashCode, toString > } > ``` > ### Deconstruction and reconstruction > Like records, carrier classes automatically acquire deconstruction patterns that > match the canonical constructor, so we can destructure our `Point` class as if > it were a record: > case Point(var x, var y): > Because reconstruction (`with`) derives from a canonical constructor and > corresponding deconstruction pattern, when we support reconstruction of records, > we will also be able to do so for carrier classes: > point = point with { x = 3; } > ## Carrier interfaces > A state description makes sense on interfaces as well. It makes the statement > that the state description is a complete, canonical, nominal description of the > interface's state (subclasses are allowed to add additional state), and > accordingly, implementations must provide accessor methods for the components. > This enables such interfaces to participate in pattern matching: > ``` > interface Pair(T first, U second) { > // implicit abstract accessors for first() and second() > } > ... > if (o instanceof Pair(var a, var b)) { ... } > ``` > Along with the upcoming feature for pattern assignment in foreach-loop headers, > if `Map.Entry` became a carrier interface (which it will), we would be able to > iterate a `Map` like: > for (Map.Entry(var key, var val) : map.entrySet()) { ... } > It is a common pattern in libraries to export an interface that is sealed to a > single private implementation. In this pattern, the interface and > implementation can share a common state description: > ``` > public sealed interface Pair(T first, U second) { } > private record PairImpl(T first, U second) implements Pair { } > ``` > Compared to the old way of doing this, we get enhanced semantics, better type > checking, and more concision. > ### Extension > The main obligation of a carrier class author is to ensure that the fundamental > claim -- that the state description is a complete, canonical, nominal > description of the object's state -- is actually true. This does not rule out > having the representation of a carrier class spread out over a hierarchy, so > unlike records, carrier classes are not required to be final or concrete, nor > are they restricted in their extension. > There are several cases that arise when carrier classes can participate in > extension: > - A carrier class extends a non-carrier class; > - A non-carrier class extends a carrier class; > - A carrier class extends another carrier class, where all of the superclass > components are subsumed by the subclass state description; > - A carrier class extends another carrier class, but there are one or more > superclass components that are not subsumed by the subclass state > description. > Extending a non-carrier class with a carrier class will usually be motiviated by > the desire to "wrap" a state description around an existing hierarchy which we > cannot or do not want to modify directly, but we wish to gain the benefits of > deconstruction and reconstruction. Such an implementation would have to ensure > that the class actually conforms to the state description, and that the > canonical constructor and component accessors are implemented. > When one carrier class extends another, the more straightforward case is that it > simply adds new components to the state description of the superclass. For > example, given our `Point` class: > ``` > class Point(int x, int y) { > component int x; > component int y; > // everything else for free! > } > ``` > we can use this as the base class for a 3d point class: > ``` > class Point3d(int x, int y, int z) extends Point { > component int z; > Point3d { > super(x, y); > } > } > ``` > In this case -- because the superclass components are all part of the subclass > state description -- we can actually omit the constructor as well, because we > can derive the association between subclass components and superclass > components, and thereby derive the needed super-constructor invocation. So we > could actually write: > ``` > class Point3d(int x, int y, int z) extends Point { > component int z; > // everything else for free! > } > ``` > One might think that we would need some marking on the `x` and `y` components of > `Point3d` to indicate that they map to the corresponding components of `Point`, > as we did for associating component fields with their corresponding components. > But in this case, we need no such marking, because there is no way that an `int > x` component of `Point` and an `int x` component of its subclass could possibly > refer to different things -- since they both are tied to the same `int x()` > accessor methods. So we can safely infer which subclass components are managed > by superclasses, just by matching up their names and types. > In the other carrier-to-carrier extension case, where one or more superclass > components are _not_ subsumed by the subclass state description, it is necessary > to provide an explicit `super` constructor call in the subclass constructor. > A carrier class may be also declared abstract; the main effect of this is that > we will not derive `Object` method implementations, instead leaving that for the > subclass to do. > ### Abstract records > This framework also gives us an opportunity to relax one of the restrictions on > records: that records can't extend anything other than `java.lang.Record`. We > can also allow records to be declared `abstract`, and for records to extend > abstract records. > Just as with carrier classes that extend other carrier classes, there are two > cases: when the component list of the superclass is entirely contained within > that of the subclass, and when one or more superclass components are derived > from subclass components (or are constant), but are not components of the > subclass itself. And just as with carrier classes, the main difference is > whether an explicit `super` call is required in the subclass constructor. > When a record extends an abstract record, any components of the subclass that > are also components of the superclass do not implicitly get component fields in > the subclass (because they are already in the superclass), and they inherit the > accessor methods from the superclass. > ### Records are carriers too > With this framework in place, records can now be seen to be "just" carrier > classes that are implicitly final, extend `java.lang.Record`, that implicitly > have private final component fields for each component, and can have no other > fields. > ## Migration compatibility > There will surely be some existing classes that would like to become carrier > classes. This is a compatible migration as long as none of the mandated members > conflict with existing members of the class, and the class adheres to the > requirement that the state description is a complete, canonical, and nominal > description of the object state. > ### Compatible evolution of records and carrier classes > To date, libraries have been reluctant to use records in public APIs because > of the difficulty of evolving them compatibly. For a record: > ``` > record R(A a, B b) { } > ``` > that wants to evolve by adding new components: > ``` > record R(A a, B b, C c, D d) { } > ``` > we have several compatibility challenges to manage. As long as we are only > adding and not removing/renaming, accessor method invocations will continue to > work. And existing constructor invocations can be allowed to continue work by > explicitly adding back a constructor that has the old shape: > ``` > record R(A a, B b, C c, D d) { > // Explicit constructor for old shape required > public R(A a, B b) { > this(a, b, DEFAULT_C, DEFAULT_D); > } > } > ``` > But, what can we do about existing uses of record _patterns_? While the > translation of record patterns would make adding components binary-compatible, > it would not be source-compatible, and there is no way to explicitly add a > deconstruction pattern for the old shape as we did with the constructor. > We can take advantage of the simplification offered by there being _only_ the > canonical deconstruction pattern, and allow uses of deconstruction patterns to > supply nested patterns for any _prefix_ of the component list. So for the > evolved record R: > case R(P1, P2) > would be interpreted as: > case R(P1, P2, _, _) > where `_` is the match-all pattern. This means that one can compatibly evolve a > record by only adding new components at the end, and adding a suitable > constructor for compatibility with existing constructor invocations. -------------- next part -------------- An HTML attachment was scrubbed... URL: From viktor.klang at oracle.com Sat Jan 17 16:00:41 2026 From: viktor.klang at oracle.com (Viktor Klang) Date: Sat, 17 Jan 2026 17:00:41 +0100 Subject: Data Oriented Programming, Beyond Records In-Reply-To: <1085557496.17567763.1768646213005.JavaMail.zimbra@univ-eiffel.fr> References: <1085557496.17567763.1768646213005.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <7c1c965e-be3a-45fb-a0ff-121fbd987a33@oracle.com> 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. 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 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). -- Cheers, ? Viktor Klang Software Architect, Java Platform Group Oracle -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Sat Jan 17 16:46:11 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Sat, 17 Jan 2026 11:46:11 -0500 Subject: Data Oriented Programming, Beyond Records In-Reply-To: <1085557496.17567763.1768646213005.JavaMail.zimbra@univ-eiffel.fr> References: <1085557496.17567763.1768646213005.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <46bd050a-4d6a-41fe-a663-a0c5cd77c766@oracle.com> > In my opinion, providing a way to automatically generate > equals/hashCode and toString() for a mutable class is just a giant > footgun. This is actually one of the fundamental design questions here, so I'm glad you brought it up.? (But I will point out that the word "footgun" is not a magic wand; the claim that "there is risk" does not, in itself, mean the approach is flawed.? Very often there is risk in both directions, and we have to choose the lesser.) Shall I assume that, modulo the handling of carriers with mutable fields, you agree with the rest?? I would be happy to have only one topic to discuss. > With a mutable class with equals/hashCode/toString generated, it's too > easy to store an object in a collection, mutate it, and then never > been able to find it again. Yes, but also: everyone here knows about this risk.? You don't need to belabor the example :) This is a reflection of a problem we already have: equals is a semantic part of the type's definition, about when two instances represent the "same" value, and mutability is pat of the type's definition, and "whether you put it in a hash-based collection and then mutate it" is about _how the instances are used by clients_. While immutability is a good default, its not always _wrong_ to use mutability; its just riskier.? And for a mutable class, state-based equality is _still_ a sensible possible implementation of equality; its just riskier.? And putting mutable objects in hash-based collections is also not wrong; its just riskier.? For the bad thing to happen, all of these have to happen _and then it has to be mutated_.? But if we have to assign primary blame here, it is not the guy who didn't write `final` on the fields, and not the guy who said that equality was state-based, but the guy who put it in the collection and mutated it. If we decided that avoiding this risk were the primary design goal, then we would have to either disallow mutable fields, or change the way we define the default equals/hashCode behavior.? Potentially ways to do the latter include: ?- never provide a default implementation, inherit the object default ?- don't provide a default implementation if there are any mutable fields ?- leave mutable fields out of the default implementation, but use the other fields While "disallow mutable fields" is a potentially principled answer, it is pretty restrictive.? Of the others, I claim that the proposed behavior is better than any of them. Carrier classes are about data, and come with a semantic claim: that the state description is a complete, canonical description of the state.? It seems pretty questionable then to use identity equality for such a class.? But the other two alternatives listed are both some form of "action at a distance", harder to keep track of, are still only guesses at what the user actually wants.? The two principled options are "don't provide equals/hashCode", and "state-based equals/hashCode", and of the two, the latter makes much more sense. It is not a bug to put a mutable object in a HashSet; it is a bug to do that _and_ to later mutate it.? So detuning the semantics of carriers, from something simple and principled to something complicated and which is just a guess about what the user really wants, just because someone might do two things that are each individually OK but together not OK, seems like an over-rotation. > So I disagree that the component can be non final and that the class > can be non final (you need those two to be non-modifiable). For your code, sure.? And for mine, most of the time.? But you are saying that "no one should be allowed to have mutable carriers"? > As Ganapathi Vara Prasad said, maybe you want the reverse semantics, > instead of indicating the components, you may want to indicate the > non-component fields, the derived fields. But for me, it does not > seems useful enough. This proposal seemed more about code golf? > 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. Careful there.? A deconstructor is like a constructor; it applies to an instance, BUT it cannot be inherited.? It is not like an instance method. -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Sat Jan 17 22:09:53 2026 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Sat, 17 Jan 2026 23:09:53 +0100 (CET) Subject: Data Oriented Programming, Beyond Records In-Reply-To: <7c1c965e-be3a-45fb-a0ff-121fbd987a33@oracle.com> References: <1085557496.17567763.1768646213005.JavaMail.zimbra@univ-eiffel.fr> <7c1c965e-be3a-45fb-a0ff-121fbd987a33@oracle.com> Message-ID: <1864311863.19305629.1768687793277.JavaMail.zimbra@univ-eiffel.fr> > From: "Viktor Klang" > To: "Remi Forax" , "Brian Goetz" > Cc: "amber-spec-experts" > 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 first, S second) { public Pair of(F first, S second) { record Impl(F first, S second) implements Pair{ } 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: From brian.goetz at oracle.com Sun Jan 18 01:00:19 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Sat, 17 Jan 2026 20:00:19 -0500 Subject: Data Oriented Programming, Beyond Records In-Reply-To: <1864311863.19305629.1768687793277.JavaMail.zimbra@univ-eiffel.fr> References: <1085557496.17567763.1768646213005.JavaMail.zimbra@univ-eiffel.fr> <7c1c965e-be3a-45fb-a0ff-121fbd987a33@oracle.com> <1864311863.19305629.1768687793277.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <51739d3f-df00-47bd-a001-08b409f64532@oracle.com> 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. On 1/17/2026 5:09 PM, forax at univ-mlv.fr wrote: > > > ------------------------------------------------------------------------ > > *From: *"Viktor Klang" > *To: *"Remi Forax" , "Brian Goetz" > > *Cc: *"amber-spec-experts" > *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 first, S second) { > ? public Pair of(F first, S second) { > ? ? record Impl(F first, S second) implements Pair{ } > ? ? 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: From forax at univ-mlv.fr Sun Jan 18 12:49:00 2026 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Sun, 18 Jan 2026 13:49:00 +0100 (CET) Subject: Data Oriented Programming, Beyond Records In-Reply-To: <51739d3f-df00-47bd-a001-08b409f64532@oracle.com> References: <1085557496.17567763.1768646213005.JavaMail.zimbra@univ-eiffel.fr> <7c1c965e-be3a-45fb-a0ff-121fbd987a33@oracle.com> <1864311863.19305629.1768687793277.JavaMail.zimbra@univ-eiffel.fr> <51739d3f-df00-47bd-a001-08b409f64532@oracle.com> Message-ID: <1710737945.19433405.1768740540151.JavaMail.zimbra@univ-eiffel.fr> > From: "Brian Goetz" > To: "Remi Forax" , "Viktor Klang" > Cc: "amber-spec-experts" > 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 | >>> ] >>> To: "Remi Forax" [ mailto:forax at univ-mlv.fr | ] , "Brian >>> Goetz" [ mailto:brian.goetz at oracle.com | ] >>> Cc: "amber-spec-experts" [ mailto: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 first, S second) { >> public Pair of(F first, S second) { >> record Impl(F first, S second) implements Pair{ } >> 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: From brian.goetz at oracle.com Sun Jan 18 16:57:48 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Sun, 18 Jan 2026 11:57:48 -0500 Subject: Data Oriented Programming, Beyond Records In-Reply-To: <1710737945.19433405.1768740540151.JavaMail.zimbra@univ-eiffel.fr> References: <1085557496.17567763.1768646213005.JavaMail.zimbra@univ-eiffel.fr> <7c1c965e-be3a-45fb-a0ff-121fbd987a33@oracle.com> <1864311863.19305629.1768687793277.JavaMail.zimbra@univ-eiffel.fr> <51739d3f-df00-47bd-a001-08b409f64532@oracle.com> <1710737945.19433405.1768740540151.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <071138f8-ba07-465a-94be-3e51c2d91ffb@oracle.com> 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" > *To: *"Remi Forax" , "Viktor Klang" > > *Cc: *"amber-spec-experts" > *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" > *To: *"Remi Forax" , "Brian Goetz" > > *Cc: *"amber-spec-experts" > > *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 first, S second) { > ? public Pair of(F first, S second) { > ? ? record Impl(F first, S second) implements Pair{ } > ? ? 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: From viktor.klang at oracle.com Mon Jan 19 00:08:05 2026 From: viktor.klang at oracle.com (Viktor Klang) Date: Mon, 19 Jan 2026 01:08:05 +0100 Subject: Data Oriented Programming, Beyond Records In-Reply-To: <071138f8-ba07-465a-94be-3e51c2d91ffb@oracle.com> References: <1085557496.17567763.1768646213005.JavaMail.zimbra@univ-eiffel.fr> <7c1c965e-be3a-45fb-a0ff-121fbd987a33@oracle.com> <1864311863.19305629.1768687793277.JavaMail.zimbra@univ-eiffel.fr> <51739d3f-df00-47bd-a001-08b409f64532@oracle.com> <1710737945.19433405.1768740540151.JavaMail.zimbra@univ-eiffel.fr> <071138f8-ba07-465a-94be-3e51c2d91ffb@oracle.com> Message-ID: 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" >> *To: *"Remi Forax" , "Viktor Klang" >> >> *Cc: *"amber-spec-experts" >> *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" >> *To: *"Remi Forax" , "Brian Goetz" >> >> *Cc: *"amber-spec-experts" >> >> *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 first, S second) { >> ? public Pair of(F first, S second) { >> ? ? record Impl(F first, S second) implements Pair{ } >> ? ? 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: From forax at univ-mlv.fr Mon Jan 19 07:45:41 2026 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Mon, 19 Jan 2026 08:45:41 +0100 (CET) Subject: Data Oriented Programming, Beyond Records In-Reply-To: References: <1085557496.17567763.1768646213005.JavaMail.zimbra@univ-eiffel.fr> <7c1c965e-be3a-45fb-a0ff-121fbd987a33@oracle.com> <1864311863.19305629.1768687793277.JavaMail.zimbra@univ-eiffel.fr> <51739d3f-df00-47bd-a001-08b409f64532@oracle.com> <1710737945.19433405.1768740540151.JavaMail.zimbra@univ-eiffel.fr> <071138f8-ba07-465a-94be-3e51c2d91ffb@oracle.com> Message-ID: <1047076842.19723466.1768808741384.JavaMail.zimbra@univ-eiffel.fr> > From: "Viktor Klang" > To: "Brian Goetz" , "Remi Forax" > Cc: "amber-spec-experts" > 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 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 | ] >>>> To: "Remi Forax" [ mailto:forax at univ-mlv.fr | ] , "Viktor >>>> Klang" [ mailto:viktor.klang at oracle.com | ] >>>> Cc: "amber-spec-experts" [ mailto: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 | >>>>>> ] >>>>>> To: "Remi Forax" [ mailto:forax at univ-mlv.fr | ] , "Brian >>>>>> Goetz" [ mailto:brian.goetz at oracle.com | ] >>>>>> Cc: "amber-spec-experts" [ mailto: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 first, S second) { >>>>> public Pair of(F first, S second) { >>>>> record Impl(F first, S second) implements Pair{ } >>>>> 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: From viktor.klang at oracle.com Mon Jan 19 12:21:54 2026 From: viktor.klang at oracle.com (Viktor Klang) Date: Mon, 19 Jan 2026 13:21:54 +0100 Subject: [External] : Re: Data Oriented Programming, Beyond Records In-Reply-To: <1047076842.19723466.1768808741384.JavaMail.zimbra@univ-eiffel.fr> References: <1085557496.17567763.1768646213005.JavaMail.zimbra@univ-eiffel.fr> <7c1c965e-be3a-45fb-a0ff-121fbd987a33@oracle.com> <1864311863.19305629.1768687793277.JavaMail.zimbra@univ-eiffel.fr> <51739d3f-df00-47bd-a001-08b409f64532@oracle.com> <1710737945.19433405.1768740540151.JavaMail.zimbra@univ-eiffel.fr> <071138f8-ba07-465a-94be-3e51c2d91ffb@oracle.com> <1047076842.19723466.1768808741384.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <3eb46c54-9c36-4471-aee5-d512c5dcf305@oracle.com> 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" > *To: *"Brian Goetz" , "Remi Forax" > > *Cc: *"amber-spec-experts" > *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 > > > 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" > *To: *"Remi Forax" , "Viktor Klang" > > *Cc: *"amber-spec-experts" > > *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" > *To: *"Remi Forax" , "Brian > Goetz" > *Cc: *"amber-spec-experts" > > *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 first, S second) { > ? public Pair of(F first, S second) { > ? ? record Impl(F first, S second) > implements Pair{ } > ? ? 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: From brian.goetz at oracle.com Mon Jan 19 14:55:07 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Mon, 19 Jan 2026 09:55:07 -0500 Subject: Data Oriented Programming, Beyond Records In-Reply-To: <1047076842.19723466.1768808741384.JavaMail.zimbra@univ-eiffel.fr> References: <1085557496.17567763.1768646213005.JavaMail.zimbra@univ-eiffel.fr> <7c1c965e-be3a-45fb-a0ff-121fbd987a33@oracle.com> <1864311863.19305629.1768687793277.JavaMail.zimbra@univ-eiffel.fr> <51739d3f-df00-47bd-a001-08b409f64532@oracle.com> <1710737945.19433405.1768740540151.JavaMail.zimbra@univ-eiffel.fr> <071138f8-ba07-465a-94be-3e51c2d91ffb@oracle.com> <1047076842.19723466.1768808741384.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <6cbaee3b-322a-4257-aa85-9df7a8574225@oracle.com> 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.) ?- 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. 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"? On 1/19/2026 2:45 AM, forax at univ-mlv.fr wrote: > > > ------------------------------------------------------------------------ > > *From: *"Viktor Klang" > *To: *"Brian Goetz" , "Remi Forax" > > *Cc: *"amber-spec-experts" > *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 > > > 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" > *To: *"Remi Forax" , "Viktor Klang" > > *Cc: *"amber-spec-experts" > > *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" > *To: *"Remi Forax" , "Brian > Goetz" > *Cc: *"amber-spec-experts" > > *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 first, S second) { > ? public Pair of(F first, S second) { > ? ? record Impl(F first, S second) > implements Pair{ } > ? ? 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: From forax at univ-mlv.fr Mon Jan 19 18:40:50 2026 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Mon, 19 Jan 2026 19:40:50 +0100 (CET) Subject: Data Oriented Programming, Beyond Records In-Reply-To: <6cbaee3b-322a-4257-aa85-9df7a8574225@oracle.com> References: <1864311863.19305629.1768687793277.JavaMail.zimbra@univ-eiffel.fr> <51739d3f-df00-47bd-a001-08b409f64532@oracle.com> <1710737945.19433405.1768740540151.JavaMail.zimbra@univ-eiffel.fr> <071138f8-ba07-465a-94be-3e51c2d91ffb@oracle.com> <1047076842.19723466.1768808741384.JavaMail.zimbra@univ-eiffel.fr> <6cbaee3b-322a-4257-aa85-9df7a8574225@oracle.com> Message-ID: <1884486700.20372636.1768848050872.JavaMail.zimbra@univ-eiffel.fr> > From: "Brian Goetz" > To: "Remi Forax" , "Viktor Klang" > Cc: "amber-spec-experts" > 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 | >>> ] >>> To: "Brian Goetz" [ mailto:brian.goetz at oracle.com | ] , >>> "Remi Forax" [ mailto:forax at univ-mlv.fr | ] >>> Cc: "amber-spec-experts" [ mailto: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 | ] >>>>>> To: "Remi Forax" [ mailto:forax at univ-mlv.fr | ] , "Viktor >>>>>> Klang" [ mailto:viktor.klang at oracle.com | ] >>>>>> Cc: "amber-spec-experts" [ mailto: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 | >>>>>>>> ] >>>>>>>> To: "Remi Forax" [ mailto:forax at univ-mlv.fr | ] , "Brian >>>>>>>> Goetz" [ mailto:brian.goetz at oracle.com | ] >>>>>>>> Cc: "amber-spec-experts" [ mailto: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 first, S second) { >>>>>>> public Pair of(F first, S second) { >>>>>>> record Impl(F first, S second) implements Pair{ } >>>>>>> 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: From brian.goetz at oracle.com Tue Jan 20 01:23:13 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Mon, 19 Jan 2026 20:23:13 -0500 Subject: Data Oriented Programming, Beyond Records In-Reply-To: <1884486700.20372636.1768848050872.JavaMail.zimbra@univ-eiffel.fr> References: <1864311863.19305629.1768687793277.JavaMail.zimbra@univ-eiffel.fr> <51739d3f-df00-47bd-a001-08b409f64532@oracle.com> <1710737945.19433405.1768740540151.JavaMail.zimbra@univ-eiffel.fr> <071138f8-ba07-465a-94be-3e51c2d91ffb@oracle.com> <1047076842.19723466.1768808741384.JavaMail.zimbra@univ-eiffel.fr> <6cbaee3b-322a-4257-aa85-9df7a8574225@oracle.com> <1884486700.20372636.1768848050872.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <7adec697-d9af-4399-ad32-ace59a73ce68@oracle.com> > > 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. You know the rule; mention syntax and you forfeit the right to more substantial comments.... > 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. "Generation" is not the right level for this discussion, though. This feature is not a Lombok macro generator; it is a semantic feature.? Can you restate this in terms of what you think such as class should _mean_? > > 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. > OK, that's good!? Let's talk about those tradeoffs -- but let's do so in terms of semantics, not generation? -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Tue Jan 20 08:17:20 2026 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Tue, 20 Jan 2026 09:17:20 +0100 (CET) Subject: Data Oriented Programming, Beyond Records In-Reply-To: <7adec697-d9af-4399-ad32-ace59a73ce68@oracle.com> References: <1710737945.19433405.1768740540151.JavaMail.zimbra@univ-eiffel.fr> <071138f8-ba07-465a-94be-3e51c2d91ffb@oracle.com> <1047076842.19723466.1768808741384.JavaMail.zimbra@univ-eiffel.fr> <6cbaee3b-322a-4257-aa85-9df7a8574225@oracle.com> <1884486700.20372636.1768848050872.JavaMail.zimbra@univ-eiffel.fr> <7adec697-d9af-4399-ad32-ace59a73ce68@oracle.com> Message-ID: <476261784.20721726.1768897040954.JavaMail.zimbra@univ-eiffel.fr> > From: "Brian Goetz" > To: "Remi Forax" > Cc: "Viktor Klang" , "amber-spec-experts" > > Sent: Tuesday, January 20, 2026 2:23:13 AM > Subject: Re: Data Oriented Programming, Beyond Records >> 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. > You know the rule; mention syntax and you forfeit the right to more substantial > comments.... I'm talking about the semantics, especially the fact that equals/hashCode and toString() are derived from. >> 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. > "Generation" is not the right level for this discussion, though. This feature is > not a Lombok macro generator; it is a semantic feature. Can you restate this in > terms of what you think such as class should _mean_? As far as i understand, the carrier class syntax class Point(int x, int y) { ... } means that - if it's not a concrete class, the implementation should provide an implementation for the canonical constructor and accessors, - if it's an interface or an abstract class, the accessors are generated as abstract methods. Then declaring a field as "component" generates the canonical constructor, the corresponding accessor and toString/equals/hashCode. I see those two features as separated, and i am not convince that the latter is worth it. For me, all the juice come from the carrier class semantics. As i said earlier, I think that providing an implementation for toString/equals/hashCode is not a good idea. Given that the class can be mutable, generating equals and hashCode hide important details that are necessary for debugging and I also think it does not makes no sense for an enum (you did not answer that question ?). regards, R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Tue Jan 20 08:56:32 2026 From: forax at univ-mlv.fr (Remi Forax) Date: Tue, 20 Jan 2026 09:56:32 +0100 (CET) Subject: Beyond records - construction vs re-construction Message-ID: <2034249756.20763062.1768899392098.JavaMail.zimbra@univ-eiffel.fr> Hello, the JEP 468 proposes a mechanism by which a new record instance can be derived from an existing one, one problem of proposing that feature as a building block is that we know that people will abuse it to use to create a record from an empty record. Instead of fighting that, for me it shows that the reconstruction proposed by JEP 468 is not the right building block. As state by Brian in the recond mail "Data oriented programmaing - Beyond records", the carrier syntax provides a state decription and it can be used not only to reconstruct an instance but also simply to construct an instance. For the rest of this mail, i will use the following class as an example final class MutablePoint(int x, int y) { private int x, y; public Point() { this.x = x; this.y = y; super(); } public int x() { return x; } public void x(int x) { this.x = x; } public int y() { return y; } public void y(int y) { this.y = y; } } *Construction* Currently Java the only way to create an instance is to call the constructor in a positional way var point = new MutablePoint(0, 0); For a carrier class, given that we have a state description, the idea is to introduce an instance construction based on states With each state being explicitly initialized. For example using a made-up syntax var point = MutablePoint { x = 0, y = 0 }; This is not new, most languages already have this kind of syntax (C, JavaScript, Rust etc) and we now that this is a missing piece in Java, because people goes as far a writing one builder class per data class for that. ? believe this is our building block to also re-construct instance. *Re-construction* The idea is that instead of providing an expression for all states, we add a way to complement using states on an already existing instance. again, not a new idea, something similar is done in JavaScript or Rust. Using our made-up syntax, we get something like this var point1 = MutablePoint { x = 1, y = 2 }; var point2 = MutablePoint { x = 2, ...point1 }; with point2.x equals to 1 and point2.y equals to 2 (the value of point1.y). It is also useful for transferring state between two different classes, by example, a mutable point and an immutable one. List mutablePoints = ... record Point(int x, int y) {?} var points = mutablePoints.stream().map(p -> Point { ...p }).collect(Collectors.toSet()); Obviously, the exact semantics has to be flesh out but i think this kind of construction semantics is more general and easier to understand thus a better building block than what is proposed by JEP 468 *. regards, R?mi * for the defence of JEP 468, using a mutable like syntax for reconstruction did made more sense when the proposed deconstruction syntax was also using a mutable like syntax. From brian.goetz at oracle.com Tue Jan 20 15:28:14 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Tue, 20 Jan 2026 10:28:14 -0500 Subject: Data Oriented Programming, Beyond Records In-Reply-To: <476261784.20721726.1768897040954.JavaMail.zimbra@univ-eiffel.fr> References: <1710737945.19433405.1768740540151.JavaMail.zimbra@univ-eiffel.fr> <071138f8-ba07-465a-94be-3e51c2d91ffb@oracle.com> <1047076842.19723466.1768808741384.JavaMail.zimbra@univ-eiffel.fr> <6cbaee3b-322a-4257-aa85-9df7a8574225@oracle.com> <1884486700.20372636.1768848050872.JavaMail.zimbra@univ-eiffel.fr> <7adec697-d9af-4399-ad32-ace59a73ce68@oracle.com> <476261784.20721726.1768897040954.JavaMail.zimbra@univ-eiffel.fr> Message-ID: > 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. > > > You know the rule; mention syntax and you forfeit the right to > more substantial comments.... > > > I'm talking about the semantics, especially the fact that > equals/hashCode and toString() are derived from. Except that equals/hashCode have nothing to do with the "component" modifier _at all_.? They are derived from the _state description_, in terms of the _accessors_, whose existence is implied directly by the _state description_. If a concrete class (including records) have a state description, then equality means "all the components are equal".? The different between carrier classes and records is the plumbing between the representation and API; one can be automated, the other might require manual assistance to complete. > As far as i understand, the carrier class syntax > ? class Point(int x, int y) { ... } > means that > - if it's not a concrete class, the implementation should provide an > implementation for the canonical constructor and accessors, > - if it's an interface or an abstract class, the accessors are > generated as abstract methods. > > Then declaring a field as "component" generates the canonical > constructor, the corresponding accessor and toString/equals/hashCode. This isn't quite right. ?- The state description implies the existence of specific API elements: canonical constructor (for constructible entities) and accessors. ?- Deconstruction is derived from the accessors. ?- Since the state description is complete, the default equals/hashCode/toString is again derived from the accessors (for concrete entities). ?- If the user provides the required API elements, nothing else is "generated". If the required API elements are not present, and they _can_ be derived, they are.? This is always true for records.? For accessors, this is true when the component is backed by a `component` field, otherwise the compiler will force you to explicitly declare it.? For constructors, if _no_ constructor is specified, an empty compact constructor will be derived.? Further, for a compact constructor, if a field is a component field, the component parameter will be implicitly committed to the field at the end of the constructor. So the role of `component` fields is limited to two things: if there is no accessor, you get one, and the compact constructor will implicitly commit the field for you.? A record is a carrier class which has component fields for all components. > As i said earlier, I think that providing an implementation for > toString/equals/hashCode is not a good idea. What this implies is that you you don't buy the (quite fundamental!) notion that a record is a degenerate case of a carrier.? Currently, a record is equivalent to a carrier class that (a) extends Record, (b) is final, and (c) has private final component fields for each component.? If you want to "separate" the default semantics of the Object methods, you have to separate the notion of carrier from records.? Is that what you're suggesting? > Given that the class can be mutable, generating equals and hashCode > hide important details that are Please stop saying "generate".? (Actually, please stop *thinking* it.)? That's Lombok-think. -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Tue Jan 20 18:35:20 2026 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Tue, 20 Jan 2026 19:35:20 +0100 (CET) Subject: Data Oriented Programming, Beyond Records In-Reply-To: References: <1047076842.19723466.1768808741384.JavaMail.zimbra@univ-eiffel.fr> <6cbaee3b-322a-4257-aa85-9df7a8574225@oracle.com> <1884486700.20372636.1768848050872.JavaMail.zimbra@univ-eiffel.fr> <7adec697-d9af-4399-ad32-ace59a73ce68@oracle.com> <476261784.20721726.1768897040954.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <1042190625.21334996.1768934120738.JavaMail.zimbra@univ-eiffel.fr> > From: "Brian Goetz" > To: "Remi Forax" > Cc: "Viktor Klang" , "amber-spec-experts" > > Sent: Tuesday, January 20, 2026 4:28:14 PM > Subject: Re: Data Oriented Programming, Beyond Records >>>> 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. >>> You know the rule; mention syntax and you forfeit the right to more substantial >>> comments.... >> I'm talking about the semantics, especially the fact that equals/hashCode and >> toString() are derived from. > Except that equals/hashCode have nothing to do with the "component" modifier _at > all_. They are derived from the _state description_, in terms of the > _accessors_, whose existence is implied directly by the _state description_. I do not think you can do that, because it means that a record is not a carrier class. Do you agree that equals() and hashCode() of a record are not derived from the accessors ? regards, R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Tue Jan 20 21:43:56 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Tue, 20 Jan 2026 16:43:56 -0500 Subject: Data Oriented Programming, Beyond Records In-Reply-To: <1042190625.21334996.1768934120738.JavaMail.zimbra@univ-eiffel.fr> References: <1047076842.19723466.1768808741384.JavaMail.zimbra@univ-eiffel.fr> <6cbaee3b-322a-4257-aa85-9df7a8574225@oracle.com> <1884486700.20372636.1768848050872.JavaMail.zimbra@univ-eiffel.fr> <7adec697-d9af-4399-ad32-ace59a73ce68@oracle.com> <476261784.20721726.1768897040954.JavaMail.zimbra@univ-eiffel.fr> <1042190625.21334996.1768934120738.JavaMail.zimbra@univ-eiffel.fr> Message-ID: Fair point that we should be more precise about this.? For the record, here is how record equality currently works: BRIEF HISTORICAL DIGRESSION The `equals` and `hashCode` implementations are indeed derived from the fields, not the accessors.? To super-simplify the discussion (not to reopen it): ?- 99.9% of the time, the user will not provide explicit accessors, and in these cases, it makes no difference. ?- When a user does provide an explicit accessor, it will almost always be to perform a defensive copy. ? ?- If the thing being copied is a collection, the answer makes no difference (Collection::equals is contents-based) and using the accessor is much^2 more expensive ? ?- If the thing being copied is an array, comparing the copy would use spurious identity and would be semnatically wrong -- so in this case you _always_ have to override equals anyway, so in this case it makes no difference While this may seem like it is just guessing what users will do, this is actually rooted in the spec of Record::equals: > Indicates whether some other object is "equal to" this one. In > addition to the general contract of Object.equals, record classes must > further obey the invariant that when a record instance is "copied" by > passing the result of the record component accessor methods to the > canonical constructor, as follows: > ? ? ? R copy = new R(r.c1(), r.c2(), ..., r.cn()); So summarizing the past decision: it should never make a difference semantically whether we use the fields or the accessors.? Using the accessors is more formally correct, but in some cases, doing so would be dramatically more expensive without any benefit to the user.? So it seemed an acceptably pragmatic choice to use the fields rather than accessors. END DIGRESSION Now, the question we should be discussing is: how should this implementation reality map to the goal of "records are degenerate carriers"? (It is hard to tell what your intent is here, whether you are merely trying to capture the details or cast doubt on the stated goal.? Obviously something makes you uncomfortable, I wish we could try to identify and understand that before suggesting random changes to the mechanics.? Let's see if we can do that.) On 1/20/2026 1:35 PM, forax at univ-mlv.fr wrote: > > > ------------------------------------------------------------------------ > > *From: *"Brian Goetz" > *To: *"Remi Forax" > *Cc: *"Viktor Klang" , > "amber-spec-experts" > *Sent: *Tuesday, January 20, 2026 4:28:14 PM > *Subject: *Re: Data Oriented Programming, Beyond Records > > > > 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. > > > You know the rule; mention syntax and you forfeit the > right to more substantial comments.... > > > I'm talking about the semantics, especially the fact that > equals/hashCode and toString() are derived from. > > > Except that equals/hashCode have nothing to do with the > "component" modifier _at all_.? They are derived from the _state > description_, in terms of the _accessors_, whose existence is > implied directly by the _state description_. > > > I do not think you can do that, because it means that a record is not > a carrier class. > > Do you agree that equals() and hashCode() of a record are not derived > from the accessors ? > > regards, > R?mi > -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Wed Jan 21 17:36:40 2026 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Wed, 21 Jan 2026 18:36:40 +0100 (CET) Subject: Data Oriented Programming, Beyond Records In-Reply-To: References: <6cbaee3b-322a-4257-aa85-9df7a8574225@oracle.com> <1884486700.20372636.1768848050872.JavaMail.zimbra@univ-eiffel.fr> <7adec697-d9af-4399-ad32-ace59a73ce68@oracle.com> <476261784.20721726.1768897040954.JavaMail.zimbra@univ-eiffel.fr> <1042190625.21334996.1768934120738.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <1801122196.22401921.1769017000476.JavaMail.zimbra@univ-eiffel.fr> > From: "Brian Goetz" > To: "Remi Forax" > Cc: "Viktor Klang" , "amber-spec-experts" > > Sent: Tuesday, January 20, 2026 10:43:56 PM > Subject: Re: Data Oriented Programming, Beyond Records > Fair point that we should be more precise about this. For the record, here is > how record equality currently works: > BRIEF HISTORICAL DIGRESSION > The `equals` and `hashCode` implementations are indeed derived from the fields, > not the accessors. To super-simplify the discussion (not to reopen it): > - 99.9% of the time, the user will not provide explicit accessors, and in these > cases, it makes no difference. > - When a user does provide an explicit accessor, it will almost always be to > perform a defensive copy. > - If the thing being copied is a collection, the answer makes no difference > (Collection::equals is contents-based) and using the accessor is much^2 more > expensive > - If the thing being copied is an array, comparing the copy would use spurious > identity and would be semnatically wrong -- so in this case you _always_ have > to override equals anyway, so in this case it makes no difference > While this may seem like it is just guessing what users will do, this is > actually rooted in the spec of Record::equals: >> Indicates whether some other object is "equal to" this one. In addition to the >> general contract of Object.equals, record classes must further obey the >> invariant that when a record instance is "copied" by passing the result of the >> record component accessor methods to the canonical constructor, as follows: >> R copy = new R(r.c1(), r.c2(), ..., r.cn()); > So summarizing the past decision: it should never make a difference semantically > whether we use the fields or the accessors. Using the accessors is more > formally correct, but in some cases, doing so would be dramatically more > expensive without any benefit to the user. So it seemed an acceptably pragmatic > choice to use the fields rather than accessors. > END DIGRESSION > Now, the question we should be discussing is: how should this implementation > reality map to the goal of "records are degenerate carriers"? The other solution seems to allow a carrier class to not be restricted to only describe data so like a record equals/hashCode/toString are derived from the fields that define a component. The carrier class feature: A carrier class is a class that define "virtual" components - the accessors must be implemented or declared abstract - the canonical constructor must be implemented if the class is concrete - the record pattern works using accessors. A carrier class is not restricted to be a data class (so no equals/hashCode/toString), it can represent any class that can be constructed/deconstructed as several virtual components. The component field feature: A field can be declared as "component", i.e. implementing one of the virtual component - the field is automatically strict - the accessor corresponding to the field can be derived (if not @Override) - if at least one of the field is declared as component, equals/hashCode/toString can be derived - if at least one of the field is declared as component, the compact constructor syntax is available and inside it, all the fields marked as component are initialized automatically - if all virtual components have an associated component field, the canonical constructor can be derived regards, R?mi > On 1/20/2026 1:35 PM, [ mailto:forax at univ-mlv.fr | forax at univ-mlv.fr ] wrote: >>> From: "Brian Goetz" [ mailto:brian.goetz at oracle.com | ] >>> To: "Remi Forax" [ mailto:forax at univ-mlv.fr | ] >>> Cc: "Viktor Klang" [ mailto:viktor.klang at oracle.com | >>> ] , "amber-spec-experts" [ mailto:amber-spec-experts at openjdk.java.net | >>> ] >>> Sent: Tuesday, January 20, 2026 4:28:14 PM >>> Subject: Re: Data Oriented Programming, Beyond Records >>>>>> 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. >>>>> You know the rule; mention syntax and you forfeit the right to more substantial >>>>> comments.... >>>> I'm talking about the semantics, especially the fact that equals/hashCode and >>>> toString() are derived from. >>> Except that equals/hashCode have nothing to do with the "component" modifier _at >>> all_. They are derived from the _state description_, in terms of the >>> _accessors_, whose existence is implied directly by the _state description_. >> I do not think you can do that, because it means that a record is not a carrier >> class. >> Do you agree that equals() and hashCode() of a record are not derived from the >> accessors ? >> regards, >> R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Jan 21 18:01:52 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 21 Jan 2026 13:01:52 -0500 Subject: Data Oriented Programming, Beyond Records In-Reply-To: <1801122196.22401921.1769017000476.JavaMail.zimbra@univ-eiffel.fr> References: <6cbaee3b-322a-4257-aa85-9df7a8574225@oracle.com> <1884486700.20372636.1768848050872.JavaMail.zimbra@univ-eiffel.fr> <7adec697-d9af-4399-ad32-ace59a73ce68@oracle.com> <476261784.20721726.1768897040954.JavaMail.zimbra@univ-eiffel.fr> <1042190625.21334996.1768934120738.JavaMail.zimbra@univ-eiffel.fr> <1801122196.22401921.1769017000476.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <953fea31-1808-4d59-87db-cf8dfe634152@oracle.com> > The other solution seems to allow a carrier class to not be restricted > to only describe data so like a record equals/hashCode/toString are > derived from the fields that define a component. I'd like to stay away from the "macro generator" view of the world, and instead talk about semantics and conceptual models.? The more we describe things in terms of macro generation, the less users will be able to _see_ the underlying concepts. I believe I had already anticipated and preemptively dismissed the model you outline below, but I'm sure it was easy to miss.? But here's the problem: `component` fields are supposed to be implementation details of _how_ a carrier class achieves the class contract.? Having the semantics of equality depend on which fields are component fields has several big problems: ?- It means that clients can't reason about what equality means by looking at the class declaration. ?- It means that the behavior of a class will change visibly under harmless-looking implementation-only refactorings, such as migrating a field from a component field to some other representation. ?- Having flung open the door labeled "macro generator", users will instantly demand "I want this to be a component field to get the accessor and constructor boilerplate, but I don't want it part of equals, can I please have a modifier to condition what counts in equals and hashCode, #kthxbye".? This violates Rule Number One of the record design, which is "no generation knobs, ever".? Once you have one, you are irrevocably on the road to Lombok. So there are two stabled, principled alternatives: ?- Just don't ever try to derive equals and hashCode ?- Derive equals and hashCode similarly as for records And of course, the first means that records cannot be considered special cases of carriers.? So the latter seems a forced move. (Meta observation: it is easy, when you have your Code Generator hat on, to suggest reasonable-seeming things that turn out to be semantically questionable or surprising or inconsistent; this is why we try so hard to approach this semantics-first.) > The carrier class feature: > A carrier class is a class that define "virtual" components > - the accessors must be implemented or declared abstract > - the canonical constructor must be implemented if the class is concrete > - the record pattern works using accessors. > > A carrier class is not restricted to be a data class (so no > equals/hashCode/toString), it can represent any class that can be > constructed/deconstructed as several virtual components. > > The component field feature: > A field can be declared as "component", i.e. implementing one of the > virtual component > - the field is automatically strict > - the accessor corresponding to the field can be derived (if not > @Override) > - if at least one of the field is declared as component, > equals/hashCode/toString can be derived > - if at least one of the field is declared as component, the compact > constructor syntax is available and inside it, all the fields marked > as component are initialized automatically > - if all virtual components have an associated component field, the > canonical constructor can be derived > > regards, > R?mi > > > On 1/20/2026 1:35 PM, forax at univ-mlv.fr wrote: > > > > ------------------------------------------------------------------------ > > *From: *"Brian Goetz" > *To: *"Remi Forax" > *Cc: *"Viktor Klang" , > "amber-spec-experts" > *Sent: *Tuesday, January 20, 2026 4:28:14 PM > *Subject: *Re: Data Oriented Programming, Beyond Records > > > > 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. > > > You know the rule; mention syntax and you forfeit > the right to more substantial comments.... > > > I'm talking about the semantics, especially the fact > that equals/hashCode and toString() are derived from. > > > Except that equals/hashCode have nothing to do with the > "component" modifier _at all_.? They are derived from the > _state description_, in terms of the _accessors_, whose > existence is implied directly by the _state description_. > > > I do not think you can do that, because it means that a record > is not a carrier class. > > Do you agree that equals() and hashCode() of a record are not > derived from the accessors ? > > regards, > R?mi > > > -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Wed Jan 21 18:36:16 2026 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Wed, 21 Jan 2026 19:36:16 +0100 (CET) Subject: Data Oriented Programming, Beyond Records In-Reply-To: <953fea31-1808-4d59-87db-cf8dfe634152@oracle.com> References: <7adec697-d9af-4399-ad32-ace59a73ce68@oracle.com> <476261784.20721726.1768897040954.JavaMail.zimbra@univ-eiffel.fr> <1042190625.21334996.1768934120738.JavaMail.zimbra@univ-eiffel.fr> <1801122196.22401921.1769017000476.JavaMail.zimbra@univ-eiffel.fr> <953fea31-1808-4d59-87db-cf8dfe634152@oracle.com> Message-ID: <1481652515.22449673.1769020576885.JavaMail.zimbra@univ-eiffel.fr> > From: "Brian Goetz" > To: "Remi Forax" > Cc: "Viktor Klang" , "amber-spec-experts" > > Sent: Wednesday, January 21, 2026 7:01:52 PM > Subject: Re: Data Oriented Programming, Beyond Records >> The other solution seems to allow a carrier class to not be restricted to only >> describe data so like a record equals/hashCode/toString are derived from the >> fields that define a component. > I'd like to stay away from the "macro generator" view of the world, and instead > talk about semantics and conceptual models. The more we describe things in > terms of macro generation, the less users will be able to _see_ the underlying > concepts. > I believe I had already anticipated and preemptively dismissed the model you > outline below, but I'm sure it was easy to miss. But here's the problem: > `component` fields are supposed to be implementation details of _how_ a carrier > class achieves the class contract. Having the semantics of equality depend on > which fields are component fields has several big problems: > - It means that clients can't reason about what equality means by looking at the > class declaration. > - It means that the behavior of a class will change visibly under > harmless-looking implementation-only refactorings, such as migrating a field > from a component field to some other representation. yes, both are true, this is also true for any classes in general. > - Having flung open the door labeled "macro generator", users will instantly > demand "I want this to be a component field to get the accessor and constructor > boilerplate, but I don't want it part of equals, can I please have a modifier > to condition what counts in equals and hashCode, #kthxbye". This violates Rule > Number One of the record design, which is "no generation knobs, ever". Once you > have one, you are irrevocably on the road to Lombok. I think this is true for any proposals that generate code for implementing a carrier class, including the one you propose before. When designing records, we have established that the only reasonable proposition that can not be labelled as a code generator is ... a record. > So there are two stabled, principled alternatives: > - Just don't ever try to derive equals and hashCode > - Derive equals and hashCode similarly as for records > And of course, the first means that records cannot be considered special cases > of carriers. So the latter seems a forced move. I still think there is value to consider that a carrier class is an abstract specification that just say that it can be constructed/deconstructed but equals/hashCode are user defined. When you use only pattern matching, you do not really need equals/hashCode to be defined, you recursively pattern-match until you have primitive values. > (Meta observation: it is easy, when you have your Code Generator hat on, to > suggest reasonable-seeming things that turn out to be semantically questionable > or surprising or inconsistent; this is why we try so hard to approach this > semantics-first.) R?mi >> The carrier class feature: >> A carrier class is a class that define "virtual" components >> - the accessors must be implemented or declared abstract >> - the canonical constructor must be implemented if the class is concrete >> - the record pattern works using accessors. >> A carrier class is not restricted to be a data class (so no >> equals/hashCode/toString), it can represent any class that can be >> constructed/deconstructed as several virtual components. >> The component field feature: >> A field can be declared as "component", i.e. implementing one of the virtual >> component >> - the field is automatically strict >> - the accessor corresponding to the field can be derived (if not @Override) >> - if at least one of the field is declared as component, >> equals/hashCode/toString can be derived >> - if at least one of the field is declared as component, the compact constructor >> syntax is available and inside it, all the fields marked as component are >> initialized automatically >> - if all virtual components have an associated component field, the canonical >> constructor can be derived >> regards, >> R?mi >>> On 1/20/2026 1:35 PM, [ mailto:forax at univ-mlv.fr | forax at univ-mlv.fr ] wrote: >>>>> From: "Brian Goetz" [ mailto:brian.goetz at oracle.com | ] >>>>> To: "Remi Forax" [ mailto:forax at univ-mlv.fr | ] >>>>> Cc: "Viktor Klang" [ mailto:viktor.klang at oracle.com | >>>>> ] , "amber-spec-experts" [ mailto:amber-spec-experts at openjdk.java.net | >>>>> ] >>>>> Sent: Tuesday, January 20, 2026 4:28:14 PM >>>>> Subject: Re: Data Oriented Programming, Beyond Records >>>>>>>> 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. >>>>>>> You know the rule; mention syntax and you forfeit the right to more substantial >>>>>>> comments.... >>>>>> I'm talking about the semantics, especially the fact that equals/hashCode and >>>>>> toString() are derived from. >>>>> Except that equals/hashCode have nothing to do with the "component" modifier _at >>>>> all_. They are derived from the _state description_, in terms of the >>>>> _accessors_, whose existence is implied directly by the _state description_. >>>> I do not think you can do that, because it means that a record is not a carrier >>>> class. >>>> Do you agree that equals() and hashCode() of a record are not derived from the >>>> accessors ? >>>> regards, >>>> R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From archie.cobbs at gmail.com Wed Jan 21 18:37:51 2026 From: archie.cobbs at gmail.com (Archie Cobbs) Date: Wed, 21 Jan 2026 12:37:51 -0600 Subject: Data Oriented Programming, Beyond Records In-Reply-To: <953fea31-1808-4d59-87db-cf8dfe634152@oracle.com> References: <6cbaee3b-322a-4257-aa85-9df7a8574225@oracle.com> <1884486700.20372636.1768848050872.JavaMail.zimbra@univ-eiffel.fr> <7adec697-d9af-4399-ad32-ace59a73ce68@oracle.com> <476261784.20721726.1768897040954.JavaMail.zimbra@univ-eiffel.fr> <1042190625.21334996.1768934120738.JavaMail.zimbra@univ-eiffel.fr> <1801122196.22401921.1769017000476.JavaMail.zimbra@univ-eiffel.fr> <953fea31-1808-4d59-87db-cf8dfe634152@oracle.com> Message-ID: I feel like Remi has a good point somewhere in here, though I'm not sure I can articulate it any better than he does.... On Wed, Jan 21, 2026 at 12:02?PM Brian Goetz wrote: > So there are two stabled, principled alternatives: > > - Just don't ever try to derive equals and hashCode > - Derive equals and hashCode similarly as for records > > And of course, the first means that records cannot be considered special > cases of carriers. So the latter seems a forced move. > Hmm... why not? Can we not say: "A record is a special case of a carrier that ... and also auto-generates equals() and hashCode()" ? [ There is some implied heresy here, which is that maybe it was wrong for records to auto-generate equals/hashCode in the first place. I found it a bit surprising the first time I tried to put some public record GraphNode objects into a Set and inadvertently created a bunch of infinite loops which I had to fix by overriding equals() with == and hashCode() with System.identityHashCode(). But obviously that horse is out of the barn... ] In other words, I like Remi's conceptual idea of carrier classes being "(possibly abstract) things with virtual components". Really that's almost like saying "modernized java beans". Then a record could still be a specialized, locked down, concrete form of a carrier class. -Archie -- Archie L. Cobbs -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Jan 21 18:49:26 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 21 Jan 2026 13:49:26 -0500 Subject: Data Oriented Programming, Beyond Records In-Reply-To: <1481652515.22449673.1769020576885.JavaMail.zimbra@univ-eiffel.fr> References: <7adec697-d9af-4399-ad32-ace59a73ce68@oracle.com> <476261784.20721726.1768897040954.JavaMail.zimbra@univ-eiffel.fr> <1042190625.21334996.1768934120738.JavaMail.zimbra@univ-eiffel.fr> <1801122196.22401921.1769017000476.JavaMail.zimbra@univ-eiffel.fr> <953fea31-1808-4d59-87db-cf8dfe634152@oracle.com> <1481652515.22449673.1769020576885.JavaMail.zimbra@univ-eiffel.fr> Message-ID: > > So there are two stabled, principled alternatives: > > ?- Just don't ever try to derive equals and hashCode > ?- Derive equals and hashCode similarly as for records > > And of course, the first means that records cannot be considered > special cases of carriers.? So the latter seems a forced move. > > > I still think there is value to consider that a carrier class is an > abstract specification that just say that it can be > constructed/deconstructed but equals/hashCode are user defined. > > When you use only pattern matching, you do not really need > equals/hashCode to be defined, you recursively pattern-match until you > have primitive values. > I agree that it is a valid question to ask: "does declaring a state description for a carrier class influence the Object behaviors." And I do understand why you are attracted to this; once we leave the strict representation constraints of records, it feels somewhat less anchored, primarily because "mutability." But also, you pay a big complexity tax when the new concept is almost like but can't quite fully meet up with the old concept; it again means that refactoring from record <--> carrier comes with a significant sharp edge, and this is a big warning signal.? So we would need a much stronger reason than "hmm, kind of like it better this way" to choose this divergent path.? So far I'm not seeing it? -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Jan 21 19:34:17 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 21 Jan 2026 14:34:17 -0500 Subject: Data Oriented Programming, Beyond Records In-Reply-To: References: <6cbaee3b-322a-4257-aa85-9df7a8574225@oracle.com> <1884486700.20372636.1768848050872.JavaMail.zimbra@univ-eiffel.fr> <7adec697-d9af-4399-ad32-ace59a73ce68@oracle.com> <476261784.20721726.1768897040954.JavaMail.zimbra@univ-eiffel.fr> <1042190625.21334996.1768934120738.JavaMail.zimbra@univ-eiffel.fr> <1801122196.22401921.1769017000476.JavaMail.zimbra@univ-eiffel.fr> <953fea31-1808-4d59-87db-cf8dfe634152@oracle.com> Message-ID: > In other words,?I like Remi's conceptual idea of carrier classes being > "(possibly abstract) things with virtual components". Really that's > almost like saying "modernized java beans". Then a record could still > be a specialized, locked down, concrete form of a carrier class. It is fair to say "I want to consider a different semantic meaning for ' a class that has a state description '."? And it is also fair to question "did records get it wrong?" as part of that discussion. Indeed, there were half a dozen different potential semantics for "class with header" that were raised during the design of records, and this variety was one of the reasons its taken us this long to figure out which we like the best. One of the implicit, but deeply held, design choices made here is that the class header is a semantic statement about what this class *is*.? Records had the cutesy slogan "the state, the whole state, and nothing but the state", which may or may not be the best slogan, but it does allow you to describe record-ness pretty concisely.? We are looking to get as close to such a crisp semantic statement as possible, ideally leaving words like "generate" out of the process. The proposal we put forth is that a carrier class C for state description S *is* a carrier for the data tuple described by S, and anything more is "incidental".? This turns out to be a pretty strong statement: ?- If C is just a carrier for an S-shaped slug of data, then (for concrete C) I must be able to take an S-shaped slug of data and produce a C (canonical constructor.) ?- If C is just a carrier for an S-shaped slug of data, then I must be able to extract that slug from a C (deconstruction pattern.) ?- If C is just a carrier for an S-shaped slug of data, then taking a C apart (into an S), and creating a new C from that same S (via the constructor) _must yield an equivalent C_.? Otherwise is is not "just" a carrier for S. If you read the spec for Record::equals, you'll see that this invariant -- take it apart with accessors, put it back together unchanged with the constructor, the result must be equals() -- is written into the Record spec.? Not because we chose to "generate" equals and hashCode, but because of what the "just" in "just a carrier for S" means. If this were not the case for carriers, then it must be because carriers are making some much weaker claim about what carrier-ness means.? But you can't claim much less without giving up a lot of benefits, too.? If we backpedal on the _completeness_ claim (the state description is a complete, canonical, nominal description of the classes state), you could argue that we then have no basis for default equality semantics other than identity-based, but we also undermine the semantic basis for reconstruction -- without the completeness claim, then `x with {}` can't be seen as "give me an equivalent x". This model of "a carrier class is one that has at these properties" is a credible one (in a vacuum), but you get much less for it; you don't get reconstruction, the constructor and deconstructor can't really be considered canonical, and it is a semantic departure from recordness. From forax at univ-mlv.fr Wed Jan 21 19:48:39 2026 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Wed, 21 Jan 2026 20:48:39 +0100 (CET) Subject: Data Oriented Programming, Beyond Records In-Reply-To: References: <1042190625.21334996.1768934120738.JavaMail.zimbra@univ-eiffel.fr> <1801122196.22401921.1769017000476.JavaMail.zimbra@univ-eiffel.fr> <953fea31-1808-4d59-87db-cf8dfe634152@oracle.com> <1481652515.22449673.1769020576885.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <626263129.22463743.1769024919495.JavaMail.zimbra@univ-eiffel.fr> > From: "Brian Goetz" > To: "Remi Forax" > Cc: "Viktor Klang" , "amber-spec-experts" > > Sent: Wednesday, January 21, 2026 7:49:26 PM > Subject: Re: Data Oriented Programming, Beyond Records >>> So there are two stabled, principled alternatives: >>> - Just don't ever try to derive equals and hashCode >>> - Derive equals and hashCode similarly as for records >>> And of course, the first means that records cannot be considered special cases >>> of carriers. So the latter seems a forced move. >> I still think there is value to consider that a carrier class is an abstract >> specification that just say that it can be constructed/deconstructed but >> equals/hashCode are user defined. >> When you use only pattern matching, you do not really need equals/hashCode to be >> defined, you recursively pattern-match until you have primitive values. > I agree that it is a valid question to ask: "does declaring a state description > for a carrier class influence the Object behaviors." And I do understand why > you are attracted to this; once we leave the strict representation constraints > of records, it feels somewhat less anchored, primarily because "mutability." > But also, you pay a big complexity tax when the new concept is almost like but > can't quite fully meet up with the old concept; it again means that refactoring > from record <--> carrier comes with a significant sharp edge, and this is a big > warning signal. So we would need a much stronger reason than "hmm, kind of like > it better this way" to choose this divergent path. So far I'm not seeing it? There is a big sharp edge between a record and a class which is due to mutability, this is true inside the class but also outside, the user code when something is mutable or not is quite different .So refactoring from a mutable class to to an unmodifiable record is a not battle we should be interested in. And if we force unmodifiability on a class, we loose half of the use cases. So what are exactly the refactoring you are envisioning that have a significant sharp edges ? regards, R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Jan 21 19:54:40 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 21 Jan 2026 14:54:40 -0500 Subject: Data Oriented Programming, Beyond Records In-Reply-To: <626263129.22463743.1769024919495.JavaMail.zimbra@univ-eiffel.fr> References: <1042190625.21334996.1768934120738.JavaMail.zimbra@univ-eiffel.fr> <1801122196.22401921.1769017000476.JavaMail.zimbra@univ-eiffel.fr> <953fea31-1808-4d59-87db-cf8dfe634152@oracle.com> <1481652515.22449673.1769020576885.JavaMail.zimbra@univ-eiffel.fr> <626263129.22463743.1769024919495.JavaMail.zimbra@univ-eiffel.fr> Message-ID: > But also, you pay a big complexity tax when the new concept is > almost like but can't quite fully meet up with the old concept; it > again means that refactoring from record <--> carrier comes with a > significant sharp edge, and this is a big warning signal.? So we > would need a much stronger reason than "hmm, kind of like it > better this way" to choose this divergent path.? So far I'm not > seeing it? > > > There is a big sharp edge between a record and a class which is due to > mutability, this is true inside the class but also outside, the user > code when something is mutable or not is quite different .So > refactoring from a mutable class to to an unmodifiable record is a not > battle we should be interested in. Just because carrier class state _can_ be mutable, doesn't mean it _must_ be.? So you're skipping over the interesting case, which is: ? ? record R(int x, ...) { } and ? ? final class R(int x, ...) { private final component int x; ... }? // not equivalent to above! In your model, the class version of R is painful to write, because you have to write equals, hashCode, and toString that delegate to each of the components.? But that's not even the main point; it is that while there is no theoretical distinction between a record and a final class all of whose components are backed by final component fields, there is a big and hard-to-explain discontinuity when you start from either of those and try to refactor to the other. But you are still not justifying your preference; WHY is identity-based equality the *obviously right* choice for carriers? Be semantic please!? Tell me what you think a carrier *means*. -------------- next part -------------- An HTML attachment was scrubbed... URL: From archie.cobbs at gmail.com Wed Jan 21 19:59:23 2026 From: archie.cobbs at gmail.com (Archie Cobbs) Date: Wed, 21 Jan 2026 13:59:23 -0600 Subject: Data Oriented Programming, Beyond Records In-Reply-To: References: <6cbaee3b-322a-4257-aa85-9df7a8574225@oracle.com> <1884486700.20372636.1768848050872.JavaMail.zimbra@univ-eiffel.fr> <7adec697-d9af-4399-ad32-ace59a73ce68@oracle.com> <476261784.20721726.1768897040954.JavaMail.zimbra@univ-eiffel.fr> <1042190625.21334996.1768934120738.JavaMail.zimbra@univ-eiffel.fr> <1801122196.22401921.1769017000476.JavaMail.zimbra@univ-eiffel.fr> <953fea31-1808-4d59-87db-cf8dfe634152@oracle.com> Message-ID: On Wed, Jan 21, 2026 at 1:34?PM Brian Goetz wrote: > If you read the spec for Record::equals, you'll see that this invariant > -- take it apart with accessors, put it back together unchanged with the > constructor, the result must be equals() -- is written into the Record > spec. Not because we chose to "generate" equals and hashCode, but > because of what the "just" in "just a carrier for S" means. > That's a good requirement, and I agree that identity equality is wrong for records and carriers, but there was another way we could have achieved this requirement, i.e, by using value equality (i.e., using == on each component) instead of recursive equality (using equals() on each component) or identity equality (using == on this & that). Asking in another way, how would you describe the conceptual distinction between a record and a value class? Thanks, -Archie -- Archie L. Cobbs -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Jan 21 21:29:53 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 21 Jan 2026 16:29:53 -0500 Subject: Data Oriented Programming, Beyond Records In-Reply-To: References: <6cbaee3b-322a-4257-aa85-9df7a8574225@oracle.com> <1884486700.20372636.1768848050872.JavaMail.zimbra@univ-eiffel.fr> <7adec697-d9af-4399-ad32-ace59a73ce68@oracle.com> <476261784.20721726.1768897040954.JavaMail.zimbra@univ-eiffel.fr> <1042190625.21334996.1768934120738.JavaMail.zimbra@univ-eiffel.fr> <1801122196.22401921.1769017000476.JavaMail.zimbra@univ-eiffel.fr> <953fea31-1808-4d59-87db-cf8dfe634152@oracle.com> Message-ID: > That's a good requirement, and I agree that identity equality is wrong > for records and?carriers, but there was another way we could have > achieved this requirement, i.e, by using value equality (i.e., using > == on each component) instead of recursive equality (using equals() on > each component) or identity equality (using == on this & that). > > Asking in another way, how would you describe the conceptual > distinction between a record and a value class? There is a lot of overlap; I would expect that many records/carriers can be value records/carriers, and many value classes can be carriers/records -- but neither subsumes the other. The essence of value-ness is the lack of identity (and all that implies -- no mutability, no layout polymorphism, no monitor, no self-recursion, etc.)? The main semantic implication is that this changes the meaning of `==` (and hence the default Object::equals) to be state-based.? But this is not as broadly useful as it sounds; once you get away from the things that are truly "value all the way down" (numbers, dates, etc) and start to talk about value classes whose fields are identity objects (including, sadly, strings), then `==` reasserts its limitations.? This is imperfect but it is the least imperfect thing we can do given the historical meaning of `==` and the gazillions of lines of code that have been written around that meaning.? So while values get a "better" `==` out of the box, `==` remains a low-level mechanism best left to low-level code. The essence of record-ness is alignment of external and internal representations.? (This assumes a record _has_ an external representation, the semantics of which are being subsumed by carriers.)? For types that "just" represent possibly-constrained products, records are an ideal choice. (To complete the story, the essence of carrier-ness is that there is a state description, which describes the "external representation", and that external representation is complete, canonical, and nominal.? This puts construction, deconstruction, and reconstruction on solid ground.) Each of value-ness / record-ness means giving something up and getting something in return.? If you are willing to give up both set of things, you get both sets of benefits.? Many classes will fit into that story, such as ? ? value record Rational(int num, int denom) { ... } ? ? value record DateTime(Date date, Time time) { ... } // where Date and Time are values ? ? value record Complex(double real, double imag) { ... } (Whether or not Rational overrides equals() depends on whether the constructor normalizes to lowest terms; if the constructor reduces preemptively, then `==` is all that is needed and we can inherit Object::equals, but either way we would expect `new Rational(2,4)` and `new Rational(3,6)` to be equals.) The introduction of carriers means many of the properties that used to be properties of records are now properties of carriers (deconstruction patterns, the take-it-apart-and-put-it-together guarantee, etc); the difference between records and carriers (as proposed) is that records are a _particular example_ of carriers, but all the semantic guarantees come from carrier-ness.? Which makes your question: what is the difference between values and carriers? The difference is: ?- Carriers are tightly constrained to a specific relationship between their put-it-together behaviors (constructors) and their take-it-apart behaviors (accessors, deconstruction), that is consistent with equality, but they are allowed to use identity and all the things it implies.? Carriers get semantic benefits (e.g. reconstruction) in exchange for this deal. ?- Values can be cagier about their implementation (they can be complete black boxes), but are constrained to not use identity and all the things it implies.? Values get runtime benefits (e.g., flattening) in exchange for this deal. The deals are not mutually exclusive. -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Wed Jan 21 22:21:53 2026 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Wed, 21 Jan 2026 23:21:53 +0100 (CET) Subject: Data Oriented Programming, Beyond Records In-Reply-To: References: <1801122196.22401921.1769017000476.JavaMail.zimbra@univ-eiffel.fr> <953fea31-1808-4d59-87db-cf8dfe634152@oracle.com> <1481652515.22449673.1769020576885.JavaMail.zimbra@univ-eiffel.fr> <626263129.22463743.1769024919495.JavaMail.zimbra@univ-eiffel.fr> Message-ID: <854941818.22541217.1769034113601.JavaMail.zimbra@univ-eiffel.fr> > From: "Brian Goetz" > To: "Remi Forax" > Cc: "Viktor Klang" , "amber-spec-experts" > > Sent: Wednesday, January 21, 2026 8:54:40 PM > Subject: Re: Data Oriented Programming, Beyond Records >>> But also, you pay a big complexity tax when the new concept is almost like but >>> can't quite fully meet up with the old concept; it again means that refactoring >>> from record <--> carrier comes with a significant sharp edge, and this is a big >>> warning signal. So we would need a much stronger reason than "hmm, kind of like >>> it better this way" to choose this divergent path. So far I'm not seeing it? >> There is a big sharp edge between a record and a class which is due to >> mutability, this is true inside the class but also outside, the user code when >> something is mutable or not is quite different .So refactoring from a mutable >> class to to an unmodifiable record is a not battle we should be interested in. > Just because carrier class state _can_ be mutable, doesn't mean it _must_ be. So > you're skipping over the interesting case, which is: > record R(int x, ...) { } > and > final class R(int x, ...) { private final component int x; ... } // not > equivalent to above! > In your model, the class version of R is painful to write, because you have to > write equals, hashCode, and toString that delegate to each of the components. If you want to model something mutable you have to maintain the invariants, if you want to have some fields to be component, you have to write equals/hashCode/toString. We can provide a more declarative syntax: - for the former, the syntax can declare the preconditions and for each field how to do the defensive copy - for the latter, the syntax can declare each field that are part of the equality dance so equals/hashCode/toString can be derived. A record is easier to write because it's a sum-types, so it's unmodfiable *and* you can derived equals/hashCode/toString, that's the sweet spot. > But that's not even the main point; it is that while there is no theoretical > distinction between a record and a final class all of whose components are > backed by final component fields, there is a big and hard-to-explain > discontinuity when you start from either of those and try to refactor to the > other. You are focusing on the gap between an unmodifiable class and a record, even if we fill that gap by adding the "component" feature, the gap between modifiable and unmodifiable will still exist. And for me, i do not see why one gap is more important than the other. > But you are still not justifying your preference; WHY is identity-based equality > the *obviously right* choice for carriers? Be semantic please! Tell me what you > think a carrier *means*. For an enum, the semantics of equals() has to be ==, so a carrier enum should use the identity-based semantics. For a data class, the semantics of equals is likely to not be == so a carrier data class has to override equals/hashCode. Basically, a carrier class does not take a side on what the semantics of equals should be, both are fine depending on the use case. regards, R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From archie.cobbs at gmail.com Wed Jan 21 22:32:14 2026 From: archie.cobbs at gmail.com (Archie Cobbs) Date: Wed, 21 Jan 2026 16:32:14 -0600 Subject: Data Oriented Programming, Beyond Records In-Reply-To: References: <6cbaee3b-322a-4257-aa85-9df7a8574225@oracle.com> <1884486700.20372636.1768848050872.JavaMail.zimbra@univ-eiffel.fr> <7adec697-d9af-4399-ad32-ace59a73ce68@oracle.com> <476261784.20721726.1768897040954.JavaMail.zimbra@univ-eiffel.fr> <1042190625.21334996.1768934120738.JavaMail.zimbra@univ-eiffel.fr> <1801122196.22401921.1769017000476.JavaMail.zimbra@univ-eiffel.fr> <953fea31-1808-4d59-87db-cf8dfe634152@oracle.com> Message-ID: Thanks for the thoughtful replies & apologies for you being double-teamed in this thread today :) I like the conceptual aspects of carrier that it "reverse inherits" from record, i.e., it has a pull-apart operation, a put-together operation, and composing those two operations equals the identity operation (modulo equals() of course). I think it would have been a reasonable choice for records to use value equality instead of equals() equality, but that's a moot point now and anyway I can always @Override if needed so not a big deal. So practically speaking, at this point, I agree it makes sense for carriers to be consistent with that. The deals are not mutually exclusive. That's very good and in effect solves my problem, I think. Will one be able to declare a value record? (Is that a contradiction?) Otherwise, presumably it would have to be a concrete value carrier class... ? Thanks, -Archie -- Archie L. Cobbs -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Wed Jan 21 23:29:42 2026 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 21 Jan 2026 18:29:42 -0500 Subject: Data Oriented Programming, Beyond Records In-Reply-To: References: <6cbaee3b-322a-4257-aa85-9df7a8574225@oracle.com> <1884486700.20372636.1768848050872.JavaMail.zimbra@univ-eiffel.fr> <7adec697-d9af-4399-ad32-ace59a73ce68@oracle.com> <476261784.20721726.1768897040954.JavaMail.zimbra@univ-eiffel.fr> <1042190625.21334996.1768934120738.JavaMail.zimbra@univ-eiffel.fr> <1801122196.22401921.1769017000476.JavaMail.zimbra@univ-eiffel.fr> <953fea31-1808-4d59-87db-cf8dfe634152@oracle.com> Message-ID: <27e4caa3-8c53-4e90-ae4f-fdbe445c40d1@oracle.com> > That's very good and in effect solves my problem, I think. Will one be > able to declare a value record? (Is?that?a contradiction?) Otherwise, > presumably it would?have to be a concrete value carrier class... ? Yes, value records (and value carrier classes) will be a thing. Unfortunately it can't be the default for records, even though most records could well be value classes, because .. history.