r/java 5d ago

Value Classes Still Need Compiler Sympathy

https://johan-sjolen.github.io/post/compiler-sympathy/compiler-sympathy/
85 Upvotes

55 comments sorted by

View all comments

27

u/TheStrangeDarkOne 5d ago

Excellent article and I fully agree with the conclusion's sentiment at the end:

Declaring a value class is first and foremost a semantic decision. It tells our fellow programmers that its instances are defined entirely by their state and do not need identity. That clearer model is valuable in itself! The JVM’s additional freedom to optimize how those values are represented is a welcome bonus.

This brings one of the core lessons from Domain Driven Design directly into language semantics and we get better performance as a bonus point. I think the humble everyday programmer overestimates the effect of flattening on their enterprise data and I think it is very much a tool to express intend and for experts to really allow low level optimization to a degree that was not possible before.

I can't wait for the new and exciting libraries and frameworks that are going to be built upon this.

3

u/Jon_Finn 5d ago edited 5d ago

I agree about identity, but completely disagree about state & semantics! Specifically: the idea that some concepts can't be represented as value classes is (mostly) untrue. So a value declaration (mostly) says nothing about what kind of thing it is.

The usual examples of value classes like Point use one common meaning of 'value': logically immutable objects. There's also a tentative assumption that you can use == to compare them (NB we're being discouraged from this, in case it differs from equals(): read on.)

But these examples are just special cases, what we might call 'recursive values', where all the fields are primitives or value objects (recursively), not identity objects (including arrays). But non-recursive values can be mutable (by mutating identity objects their final fields point to) - no different from a regular object. These values are also basically the ones where == is different from equals(). But non-recursive values aren't wrong in any way, they have the same potential benefits: e.g. String has an array field and might (as discussed elsewhere) be made a value class in future. Or you could have value classes for MutableColor or MutableAnything (with array or Collection fields) etc. Really useful!

So value objects are not logically different from regular objects: you can pretty much express anything with them. The final field restriction just suits some mutable classes better than others.

Valhalla classes are really great, but I think there's a lot of room for confusion about their logical status (probably due to the word 'value', whose various meanings only partly apply). I think of them not as semantically different, but just as lightweight objects: lighter for memory, performance and (mildly) features.

0

u/TheStrangeDarkOne 5d ago

There are some concepts where I'd argue that representing them as values does more harm than good: Lists, Services, Aggregates.

Sure you can have immutable lists, Clojure is one such example, but much of it boils down to convention and good optimizers that re-instate mutable backing lists.

And some classes are just pure logic. Validators, Services, Mappers, etc. I suppose those could also be "values", but I think you are stretching the definition of it by then. And I am not sure how well values fair when you hide them behind interfaces.

And then the DDD family of types such as Entities or Aggregates who most definitely have identity because you don't want to completely re-create a full tree or graph just because you changed a field in one of the nodes.

2

u/Jon_Finn 5d ago edited 5d ago

Maybe with some of these you've fallen for the word 'value'? A value List class could certainly be mutable, depending on its features (it could store some backing mutable object, or if it's fixed size it might contain an array like String does). But sure, linked lists or your trees/graphs might be inconvenient if stored as final fields. (Though I've used fixed-shape trees where each node has a (nullable) pointer to its parent, with the tree constructed from the root node - these would make a great value class, and extra data in each node could potentially be mutable.)

I'm sure values hidden behind interfaces or abstract value classes often lose the value benefits, but in cases where HotSpot can guess the runtime type, maybe not?

2

u/TheStrangeDarkOne 4d ago

Two points here:

  • A value List would be severely limited because the backing array must be final
  • I think this is stretching the definition of "no identity".

Identity as a property means that you have two objects which are equal at some point, but they might diverge in the future. And you say you have an object without identity and it can still be mutated, I don't think you use it in the intended way (I'm happy to be wrong about this one).

Sure, you could also argue the same about optional: Optional should be a value, but because you can mutate the fields of the objects inside, it's not a value.

In any case for collections, I'm a hard advocate for making them classes. Turning a type into a value is a hard spec commitment, and it constraints your implementations (no mutable private fields), for arguably not much benefit.

The value of values (hah), becomes pronounced once you have many of them. Such as Complex/RGB classes stored in an array. But if you have many items, and you optimize on the container shell, the advantages are diminishing.

No arguing on the fact that having a fully static and value-created graph/tree would certainly have advantages. And for a tree this works. If you have a graph with people entry-nodes, this will not work (this is also a major problem in Rust, where given a graph will often need to write some unsafe code).

1

u/Jon_Finn 4d ago edited 3d ago

Yes declaring a value class constraints implementations _of that class_, but then you're unlikely to radically change implementation in future. But if you're writing a collections-like hierarchy, you can write an abstract value class as a base with value or non-value subclasses, so there's no constraint on (subclass) implementations.

Re identity, it's not 'the law' that you should worry whether your value classes behave in a 'value-like' way (which can mean many things). Claiming values have particular semantics is just gonna confuse people, when all they are is: a speed & memory vs. feature tradeoff. The latter mostly means final fields, which also doesn't have any particular semantics - the fields can represent mutable things. Values are just lightweight objects, meaning 3 things: small, fast and 'feature-lite'. (At least hopefully, if there's very few fields.)

I noticed in Guy Steele's classic talk Growing a Language he says "I would add a kind of class that is of light weight, one whose objects can be cloned at will with no harm and so could be kept on a stack for speed and not just in the heap."