Live data from Hacker News

Making Lenses Practical in Java

chriskiehl.com

21–30 of 108 posts

Re: Making Lenses Practical in Java

#21

The problem this is trying to resolve is creating copies of a complex immutable class with some deep fields changed. I do have to wonder for a case like this if using a mutable object with a deep copy function wouldn't be a better solution, rather than adding extra magic to do it more easily as possible... Edit: The code in the article would become something like pendingOrders.map(order -> { copy = order.DeepCopy();…

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.

This 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].

Re: Making Lenses Practical in Java

#22

Earlier quoted context omitted.

A 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())

I 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.

You do not need lenses for implementation of a method working with internal state, that will be a gross over-engineering.

   record Order(List 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);
   }

Re: Making Lenses Practical in Java

#23
post #19

Lenses are cool, but they make me wonder: how many different paths of a deep immutable data hierarchy are transformed in an application to make this abstraction worthwhile, as opposed to replicating the entire traversal at each location? If the number is small then, however cool, this abstraction may be more trouble than it's worth. Just because you can reify some concept in an elegant composable construct doesn't me…

I’ve been playing with this idea around the time generics arrived in Java and never found a good use case for it. Number of transitions an object may have can usually be counted by fingers on one hand, meaning that you can have a method for each one and retain encapsulation, which lenses break by design.

Re: Making Lenses Practical in Java

#24
Or just stop trying to force immutability into something that is clearly mutable. You’re only causing unnecessary GC pressure by copying objects.

Never the less this eerily looks similar to Goetz proposal for reconstructors:

https://github.com/openjdk/amber-docs/blob/master/eg-drafts/...

Re: Making Lenses Practical in Java

#25

Earlier quoted context omitted.

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.

This 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 wan…

Yeah, 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).

Re: Making Lenses Practical in Java

#26
post #19

Lenses are cool, but they make me wonder: how many different paths of a deep immutable data hierarchy are transformed in an application to make this abstraction worthwhile, as opposed to replicating the entire traversal at each location? If the number is small then, however cool, this abstraction may be more trouble than it's worth. Just because you can reify some concept in an elegant composable construct doesn't me…

The real power IMHO comes once you start using traversals, which are like lenses but focus 0..n elements instead of exactly 1, and compose well with themselves and with lenses. (It gets better still after that, as you add in Folds, Isos, Prisms, etc., but we'll leave those for now)

Once you have a traversal that can pull out immediate children of the same type (e.g., "given a DOM node, traverse all of its children"), you can use a library of transformations like Haskell's Control.Lens.Plated module from package "lens" and write queries and transformations over arbitrary structures in a very compact way.

I have used this a few times: some examples include walking complex documents to extract particular information from tables, or rewriting every "import" node in a syntax tree, but leaving the rest of the program untouched.

To support Traversals in Java under that get/set form, you would probably need get/set members that were functions to/from T and Array, and then you'd have to write separate compose operators for each composition:

- lens + lens = lens

- lens + traversal = traversal

- traversal + lens = traversal

- traversal + traversal = traversal

This is one reason that Haskell's "lens" package has that funky type alias for Lens instead of a record-of-functions, and a mature lens library is one of the main reasons that Haskell is my favourite general-purpose programming language.

You _can_ force such a take on optics into Java. Someone found an implementation of profunctor optics in Minecraft's DataFixerUpper: https://www.reddit.com/r/programming/comments/9lyplq/microso... That subthread has some really good meaty comments in it, if you're interested in this sort of thing.

Re: Making Lenses Practical in Java

#28

Earlier quoted context omitted.

I 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.

You do not need lenses for implementation of a method working with internal state, that will be a gross over-engineering. record Order(List 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);…

Lenses 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.

Re: Making Lenses Practical in Java

#29
post #24

Or just stop trying to force immutability into something that is clearly mutable. You’re only causing unnecessary GC pressure by copying objects. Never the less this eerily looks similar to Goetz proposal for reconstructors: https://github.com/openjdk/amber-docs/blob/master/eg-drafts/...

[deleted]

Re: Making Lenses Practical in Java

#30
post #9

I don't agree on the fact that lombok has brought us out of the dark ages. We used to use it, but it has some drawbacks. One of them, it's an additional dependency. This, for a simple thing such as pojo, seems a bit overkill to me. The additional amount of time used to write some cose isn't worth the risk of additional bugs hidden in having one more dependency.

Record classes arguably do a lot of the same thing without the drawbacks.

But they require a new JDK version, and a surprisingly high amount of Java projects are still stuck on JDK8.
Post reply on HN