3 ms·
The issue is that this potentially copies a lot of unnecessary information. Also, if you depend on object identities (reference equality), deep copying will rec
by ImprobableTruth 4y ago
The issue is that this potentially copies a lot of unnecessary information. Also, if you depend on object identities (reference equality), deep copying will recreate objects even if they aren't supposed to change.
Furthermore, if you want to chain two update functions, you'll deep copy parts of the object twice times.
Lenses are "smart" in the sense that they only copy what's necessary.
- ivan_gammel 4y agoA business method on order would do the same without any unnecessary overhead and ensuring proper encapsulation and consistency of the state that cannot be achieved with getters and setters. pendingOrders.map(order -> order.approvalConfirmationUpdated(now())
- kitd 4y agoI believe the intention is to allow operations on multilayered business objects to be composable. So your approvalConfirmationUpdated() method may actually be made up of a sequence of smaller operations, each of which I might want to use elsewhere in the application. Lenses allow this to happen succinctly, and works better with eg the Java streams API.
- ivan_gammel 4y agoI got that, my point is that such composability is rarely better than good old OOP with encapsulation. Enumerating all possible state transitions on object interface leads to better design than procedural programming (and composable operations are procedural programming).
- ImprobableTruth 4y agoI don't think I understand. We're talking about how to implement this. That method will still have to use deep copying + mutation, lenses or something else internally.
- ivan_gammel 4y agoYou do not need lenses for implementation of a method working with internal state, that will be a gross over-engineering. 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); }
- ImprobableTruth 4y agoLenses are an abstraction of what you mentioned. For a single example it's of course overengineering. The benefit is that e.g. when you have a bunch of methods like that, you can avoid duplicating the code that is responsible for copying the inner layers. Otherwise if you e.g. add a layer, you have to touch all those methods.
- ivan_gammel 4y agoIf you look at my example you will notice that it has zero lines that would be duplicated in real life scenarios, because it does not perform a deep copy (a benefit of using immutable objects).
- ImprobableTruth 4y agoImagine you changed it from an array of approvals to a single one like in the original example - you'd need to make a change in every method of that type (replacing the map). That's code duplication, it's just not really obvious yet because there's only two layers to pass through. Per layer you need a constructor call (or map to copy & modify the array). 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.
- eyelidlessness 4y agoThis sounds eerily similar to persistent data structures (eg in FP contexts like Clojure): updated values share as much[1] as possible with their inputs, and create new values only[1] when they differ. Granted in FP contexts, this is primarily an optimization, transparent[2] to the programmer actually using those values in a program. 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].
- ImprobableTruth 4y agoYeah, it's also originally from the FP side. There it started I believe because people were looking how to easily make immutable updates to deep structures, since the normal way is pretty boilerplate heavy in comparison to the mutation way of just chaining accessors for an update. 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).