EXPEDIA GROUP TECHNOLOGY —ENGINEERING

Lenses in Java

Vivek Jaiswal
Expedia Group Technology
4 min readDec 14, 2021

--

Improve your functional programming with Lenses

Picture of a woman with shoulder length dark hair standing in front of a red background. She is looking at you through a magnifying glass.
Photo by cottonbro from Pexels

States are a common source of coupling which can glue the layers in a codebase together. Many components depend on such shared state and when we need to modify the state, we need to touch upon at multiple places probably spanning multiple logical layers. This causes code litter at multiple places.

This is where Lenses come to the rescue.

Read on to know more about the magic of Lenses…

What are Lenses?

Immutability is the core concept of functional programming languages and this idea has found its way into object oriented programming languages like Java too. There’s no denying the fact that immutability offers a lot of benefits along with it but do come with few disadvantages. Some of the most pressing ones are:

  • allocating lots and lots of small objects rather than modifying ones you already have leading to a performance impact
  • cloning objects and collections is tricky

These issues can be addressed by using Lenses.

Lenses keeps the balance of mutability in the immutability world. It decorates accessors and mutators of an object in a functional style providing the capability to access and mutate data in an immutable manner. It hides the complexity of cloning behind the accessor and mutators.

So, if you have a complex object which has composition of various degree of hierarchy of objects and you expect that this hierarchy might need to be manipulated or even accessed. Lenses will help you to achieve it without introducing redundancy and repetition in the code.

Adding on to that, Lenses prevent the snowball effect of mutation in immutability universe and keep them localised to the section of code which was actually modified. It generalises the accessors and mutators with the addition of composition capability.

Lens implementation examples

Before we jump into the implementation of Lens design pattern, let’s come up with few example beans that we’ll help us showcase the power of Lenses.

Our example classes model a movie booking system and for the sake of simplicity store only one movie booking for the User.

Classes User, Booking, Show and Movie are connected through composition and are an ideal example to demonstrate Lenses.

Let’s have a look at the fully functional Lens pattern.

Here, we can see how the getter is a simple function that receives a whole object and returns a part of it. And, with the setter, we can create a new whole object with the new part that we have passed to it.

Lens usage

A simple Lens usage could be like below:

As we see in the test above, the get of our Lens passing a User gives us its username and using the set of our Lens, passes it a new name and returns the complete object with the changed name.

You might think that, the get points to one thing and set modifies the other. But that’s not the case, so let’s continue and find out what rules are followed by the Lenses.

Lens rules

Lens provide a convenient and easy way to mutate the state of the code. This convenience has been possible because of the rules that Lens pattern adheres to. Let’s take a look at the rules.

1. Set after get

Statement: Updating the object with what was received does not change the object

assertSame(userNameLens.set(user, userNameLens.get(user)), user);

With this rule, we should see that the set and get focus on the same part of the object.

2. Get after get

Statement: Updating the object and then receiving should return what was updated

assertEquals(userNameLens.get(userNameLens.set(user, "newName")), "newName");

In this, first set of the Lens is executed which returns a new User with new name. When we get the username via the Lens for the new User, we should get the new name.

3. Set after set

Statement: Updating the object twice should return the last updated object

assertEquals(userNameLens.set(userNameLens.set(user, "newName"), "newName2"), userNameLens.set(user, "newName2"));

In this, first the internal Lens set is executed which updates the user name to newName and then the outer Lens’ set is executed which updates it to newName2 which is also seen in the final result.

Lens composition

As stated before, Lenses can be composed together adding a tremendous value.

This is the real power of Lenses.

We can compose multiple Lenses together which results in a new Lens. The new Lens will be able to look into the object hierarchy deeper than which could be done from individual Lenses. With the composition of Lenses, the depth of the object graph can be reached in a simpler way and then mutate/fetched easily. Let’s try to understand it through an example:

In order to create a composite Lens, we create a function f that accepts two Lenses and combines them to create a new Lens. So, assume there’s a Lens 1 that talks to type A and B (where B is part of A following the composition principle in OOP), and another Lens 2 that talks to type B and C (where C is part of B). The function f will return a new Lens that talks not only to types A & B or B & C individually but also to type A & C going deep into the inner levels.

Let’s see composition of Lenses in action.

Final thoughts

Lens design pattern is an easy way to introduce the getters and setters in an immutable and functional way. With the benefit, there’s a risk of anti-pattern too where it could break the immutability concept if not used carefully.

Please find executable code with tests at https://github.com/liquidpie/lenses-java

--

--

Vivek Jaiswal
Expedia Group Technology

Software Architect | Youtuber | Ployglot Developer | Stackoverflow 1.5K+ | Marathon Runner | Find more about me at http://vivekjaiswal.me