Lenses are "smart" in the sense that they only copy what's necessary.
Lenses are "smart" in the sense that they only copy what's necessary.
From the perspective of such a programmer, I can’t think of a scenario where I’d want both value and reference equality semantics for the same objects. Is it reasonable to assume that the “smartness” here is likewise focused on making immutability perform well, rather than on use cases where value and reference equality are simultaneous considerations?
1: Handwaves away implementation details. General cautions about abstractions leaking apply.
2: Exceptions may apply[1].
And you're close, it's about using immutability to make value equality cheap by making reference equality a proxy for it. If you don't use mutations, value equality implies reference equality and is thus equivalent (since reference equality already normally implies value equality). That means you can get away with just a single pointer comparison in comparison to completely traversing both structures. This is e.g. what React does to determine whether arguments of a component have changed.
(Well, at least I also struggle to think of a scenario where both types of equality are semantically important).
pendingOrders.map(order -> order.approvalConfirmationUpdated(now()) record Order(List<Approval> approvals, int version, OrderStatus status) {
Order confirmed(Instant timestamp, UUID approver) {
var approvals = this.approvals.stream().map(a-> a.user().uuid().equals(approver) ? a.confirmed(timestamp) : a).toList();
return new Order(approvals, version++, OrderStatus.APPROVED);
}As a more obvious example, if you want to modify a.b.c.d.e (which isn't unrealistic), you'll need to call the constructors of A, B, C and D. If you don't use lenses, this is the code that will be duplicated. You can spread it between the classes or do all that in a method of A, but if you want to also modify a.b.c.d.f, you'll need to duplicate all that code (add another method to A, B, C, D that each calls the constructor).
With lenses, you define once how to access d from a and then any modification of d can happen through that. If the structure changes, you only need to do the changes once by modifying the lens.
1) change in cardinality is such a change in domain that lenses won’t solve it. There will be much bigger changes in business logic probably making original code obsolete regardless of used pattern.
2) deep tree modifications of the kind that you mentioned indicate problems with architecture. Why would you need the whole typed tree (not DOM or something, but object tree) to modify a tiny leaf of it? If you touch multiple leafs with root as closest integration point, why your data model is designed like that, pointing to strong coupling of different contexts?
3) most importantly the code that can be reused, can be extracted to a private method of an entity where the change occurs. Calling nested constructors on the root object breaks encapsulation — it should pass the message to nested objects instead.