Alternative to IdentityObject & ValueObject interfaces
Remi Forax
forax at univ-mlv.fr
Wed Mar 23 12:43:28 UTC 2022
Hi Brian,
i've maybe have twisted mind but i read your email as a rebuttal of both IdentityObject/ValueObject and identity/value modifiers.
As you said, an identity object and a value object are less dis-similar now that they were in the past: a value class now reuse the method equals and hashCode of j.l.Object instead of coming with it's own definition,
a value class is now nullable.I agree with you that synchronized is not a real issue so as Dan H. said, the real remaining issue is weak refs.
Now, if there is such a small differences between an identity object and a value object, do we really need to introduce a mechanism to separate them in term of typing ?
Rémi
> From: "Brian Goetz" <brian.goetz at oracle.com>
> To: "daniel smith" <daniel.smith at oracle.com>, "valhalla-spec-experts"
> <valhalla-spec-experts at openjdk.java.net>
> Sent: Wednesday, March 23, 2022 1:01:20 PM
> Subject: Re: Alternative to IdentityObject & ValueObject interfaces
> Thanks Dan for putting the work in to provide a credible alternative.
> Let me add some background for how we came up with these things. At some point
> we asked ourselves, what if we had identity and value classes from day 1? How
> would that affect the object model? And we concluded at the time that we
> probably wouldn't want the identity-indeterminacy of Object, but instead would
> want something like
> abstract class Object
> class IdentityObject extends Object { }
> abstract class ValueObject extends Object { }
> So the {Identity,Value}Object interfaces seemed valuable pedagogically, in that
> they make the object hierarchy reflect the language division. At the time, we
> imagined there might be methods that apply to all value objects, that could
> live in ValueObject.
> A separate factor is that we were taking operations that were previously total
> (locking, weak refs) and making them partial. This is scary! So we wanted a way
> to make these expressible in the static type system.
> Unfortunately, the interfaces do not really deliver on either goal, because we
> can't turn back time. We still have to deal with `new Object()`, so we can't
> (yet) make Object abstract. Many signatures will not be changeable from
> "Object" to "IdentityObject" for reasons of compatibility, unless we make
> IdentityObject erase to Object (which has its own problems.) If people use it
> at all for type bounds, we'll see lots of uses of `Foo<? extends
> Bar&IdentityObject>`, which will put more pressure on our weak support for
> intersection types. And dynamic errors will still happen, because too much of
> the world was built using signatures that don't express identity-ness. (Kevin
> will see a parallel to introducing nullness annotations; it might be fine if
> you build the world that way from scratch, but the transition is painful when
> you have to interpret an unadorned type as "of unspecified identity-ness.")
> Several years on, we're still leaning on the same few motivating examples --
> capturing things like "I might lock this" in the type system. That we haven't
> come up with more killer examples is notable. And I grow increasingly skeptical
> of the value of the locking example, both because this is not how concurrent
> code is written, and because we *still* have to deal with the risk of dynamic
> errors because most of the world's code has not been (and will not be) written
> to use IdentityObject throughout.
> As Dan points out, the main thing we give up by backing off from these
> interfaces is the static typing; we don't get to use `IdentityObject` as a
> parameter type, return type, or type bound. And the only reason we've come up
> with so far to want that is a pretty lame one -- locking.
> From a language design perspective, I find that you declare a class with `value
> class`, but you express the subclassing constraint with `extends
> IdentityObject`, to be pretty leaky.
> On 3/22/2022 7:56 PM, Dan Smith wrote:
>> In response to some encouragement from Remi, John, and others, I've decided to
>> take a closer look at how we might approach the categorization of value and
>> identity classes without relying on the IdentityObject and ValueObject
>> interfaces.
>> (For background, see the thread "The interfaces IdentityObject and ValueObject
>> must die" in January.)
>> These interfaces have found a number of different uses (enumerated below), while
>> mostly leaning on the existing functionality of interfaces, so there's a pretty
>> good complexity vs. benefit trade-off. But their use has some rough edges, and
>> inserting them everywhere has a nontrivial compatibility impact. Can we do
>> better?
>> Language proposal:
>> - A "value class" is any class whose instances are all value objects. An
>> "identity class" is any class whose instances are all identity objects.
>> Abstract classes can be value classes or identity classes, or neither.
>> Interfaces can be "value interfaces" or "identity interfaces", or neither.
>> - A class/interface can be designated a value class with the 'value' modifier.
>> value class Foo {}
>> abstract value class Bar {}
>> value interface Baz {}
>> value record Rec(int x) {}
>> A class/interface can be designated an identity class with the 'identity'
>> modifier.
>> identity class Foo {}
>> abstract identity class Bar {}
>> identity interface Baz {}
>> identity record Rec(int x) {}
>> - Concrete classes with neither modifier are implicitly 'identity'; abstract
>> classes with neither modifier, but with certain identity-dependent features
>> (instance fields, initializers, synchronized methods, ...) are implicitly
>> 'identity' (possibly with a warning). Other abstract classes and interfaces are
>> fine being neither (thus supporting both kinds of subclasses).
>> - The properties are inherited: if you extend a value class/interface, you are a
>> value/class interface. (Same for identity classes/interfaces.) It's an error to
>> be both.
>> - The usual restrictions apply to value classes, both concrete and abstract; and
>> also to "neither" abstract classes, if they haven't been implicitly made
>> 'identity'.
>> - An API ('Object.isValueObject()'?) allows for dynamically distinguishing
>> between value objects and identity objects. The reflection API (in
>> java.lang.Class) allows for detection of value classes/interfaces, identity
>> classes/interfaces, and "neither" classes/interfaces.
>> - TBD whether/how we track these properties statically so that the type system
>> catch mismatches between non-identity class types and uses that assume
>> identity.
>> JVM proposal:
>> - Same conceptual framework.
>> - Classes can be ACC_VALUE, ACC_IDENTITY, or neither.
>> - Legacy-version classes are implicitly ACC_IDENTITY. Legacy interfaces are not.
>> Optionally, modern-version concrete classes are also implicitly ACC_IDENTITY.
>> (Trying out this alternative approach to abstract classes: there's no more
>> ACC_PERMITS_VALUE; instead, legacy-version abstract classes are automatically
>> ACC_IDENTITY, and modern-version abstract classes permit value subclasses
>> unless they opt out with ACC_IDENTITY. It's the bytecode generator's
>> responsibility to set these flags appropriately. Conceptually cleaner, maybe
>> too risky...)
>> - At class load time, we inherit value/identity-ness and check for conflicts.
>> It's okay to have neither flag set but inherit the property from one of your
>> supers. We also enforce constraints on value classes and "neither" abstract
>> classes.
>> ---
>> So how does this score as a replacement for the list of features enabled by the
>> interfaces?
>> - Dynamic detection: 'obj instanceof ValueObject' is quite straightforward; if
>> we can replace that with 'obj.isValueObject()', that feels about equally
>> useful. (I'd be more pessimistic about something like
>> 'Objects.isValueObject(obj)'.)
>> - Subclass restriction: 'implements IdentityObject' has been replaced with the
>> 'identity' modifier. Complexity cost of special modifiers seems on par with the
>> complexity of special rules for inferring and checking the superinterfaces. I
>> think it's a win that we use the 'value' modifier and "value" terminology for
>> all kinds of classes/interfaces, not just concrete classes.
>> - Variable types: I don't see a good way to get the equivalent of an
>> 'IdentityObject' type. It would involve tracking the 'identity' property
>> through the whole type system, which seems like a huge burden for the
>> occasional "I'm not sure you can lock on that" error message. So we'd probably
>> need to be okay letting that go. Fortunately, I'm not sure it's a great
>> loss—lots of code today seems happy using 'Object' when it means, informally,
>> "object that I've created for the sole purpose of locking".
>> - Type variable bounds: this one seems more achievable, by using the 'value' and
>> 'identity' keywords to indicate a new kind of bounds check ('<identity T
>> extends Runnable>'). Again, it's added complexity, but it's more localized. We
>> should think more about the use cases, and decide if it passes the cost/benefit
>> analysis. If not, nothing else depends on this, so it could be dropped. (Or
>> left to a future, more general feature?)
>> - Documentation: we've lost the handy javadoc location to put some explanations
>> about identity & value objects in a place that curious programmers can easily
>> stumble on. Anything we want to say needs to go in JLS/JVMS (or perhaps the
>> java.lang.Object javadoc).
>> - Compatibility: pretty clear win here. No interface injection means tools that
>> depend on reflection results won't be broken. (We've found a significant number
>> of these problems in our own code/tests, FWIW.) No new static types means
>> inference results won't change. There's less risk of incompatibilities when
>> adding/removing the 'identity' and 'value' keywords (although there can still
>> be source, binary, and behavioral incompatibilities).
More information about the valhalla-spec-observers
mailing list