Lenses
Immutability
Using immutable types has a number of benefits. Since it eliminates mutation, it makes code easier to reason about. Also, it eliminates concurrent modification problems, thereby unlocking a lot of performance improvement opportunities. Java originally was fully Object-Oriented but it adapted to other paradigms. The language itself still lacks some features that makes it convenient to work with immutable data. In this post I’d like to show what can currently be done in vanilla Java and show a powerful concept to transform immutable data.
Java Records
Since Java 16, record
classes were finalised and added to the language, which are described in JEP 395 as “transparent carriers for immutable data”. The canonical example of this is a Point
in two-dimensional space. A Point
is defined by an x
and y
coordinate - nothing more, nothing less.
record Point(int x, int y) { }
Using a record
gives us a lot of functionality out of the box like proper equals
/hashcode
implementations, constructors, accessor methods, and more. The type is shallowly immutable, meaning that there is no way to reassign its components. This restriction gives us a lot of benefits - we can safely distribute instances to different parts of our logic and safely use concurrent programming without fear, since there is no way to mutate the instance itself.
Data Transformation
There are of course plenty of situations where we do need data deviating from the data that we received. For example, updating the x
value of a given Point
. We do not mutate our input, but we create a new Point
:
record Point(int x, int y) {
Point withX(int input) {
return new Point(input, this.y());
}
Point withY(int input) {
return new Point(this.x(), input);
}
}
We successfully create a new instance, but we do have to specify the value for each component explicitly, even when it is the same as the input value. When our code grows in volume (more creation methods or more record components) this becomes tedious and error-prone.
Derived Creation
To solve this we need a mechanism for derived instance creation, a way to specify that we want to create new instances with only some modifications. JEP 468 is exploring this as part of Project Amber.
One way to already get this functionality is to use Lombok @With
annotation. It generates with
methods for each record component, allowing derived creation altering only that component
@With
record Point(int x, int y) {
// withX, withY generated automatically
}
This enables convenient shallow derived creation for single components, a valuable step, but we can take it a lot further. What if we want to make deep transformations?
Deep Transformation
As an example, suppose we have record classes for Person
, House
, Address
and City
. A Person
has a House
, which has an Address
, which has a City
as follows:
@With record Person(String firstName, String lastName, House house) { }
@With record House(Address address) { }
@With record Address(String street, String zipCode, City city) { }
@With record City(String name) { }
Suppose alice
wants to move to New York. Using the most convenient strategy discussed we end up with the following:
static City NEW_YORK = new City("New York");
public static Person moveToNewYork(Person person) {
return person.withHouse(
person.house().withAddress(
person.house().address().withCity(NEW_YORK)
)
);
}
In order to provide the new City
for a Person
we have to call withHouse
, and specify explicitly that we would like to use the existing house
with a transformation to address
. Then we use the existing address
but specify a new value for city
. That was a lot of code to write, and we can see that this does not scale well when nesting grows deeper or when we have a lot of transformations that we would like to express.
Lenses
What is needed here is an API to express the intent to apply nested transformations. In Haskell
and Scala
the paradigm of immutable programming has existed for a long time and thus support has emerged to effectively work with immutable data. One option is to use a Lens
paradigm.
Before exploring what a Lens
is and how it works, lets demonstrate the above example in a Lens
style, expressing the intent to change the city of the address of the house of the person:
static City NEW_YORK = new City("New York");
public static Person moveToNewYork(Person person) {
return PersonLens.µ
.house()
.address()
.city()
.with(NEW_YORK)
.apply(person);
}
This enables a fluent style where the implementation is declarative and non-repetitive. This eliminates scaling issues with record size, nesting level, or amount of transformations.
Foundation
The design behind the above paradigm is called Lensing
. A Lens
is a type with two essential functionalities: retrieving nested data, and transforming it. This is implemented by adding these functionalities as components of a record class. The Lens
“zooms into” a child property of type T
of a parent type S
:
record Lens<S, T>(Function2<S, T, S> with, Function1<S, T> get)
Creating a Lens
To create a Lens
allowing us to change the City
of an Address
we write the following:
Lens<Address, City> addressCityLens = new Lens<>(Address::withCity, Address::city);
Writing all this by hand sounds like effort, so…
Automatic Lenses
I’ve created a library to automate the above using annotation processing. All that is required is annotating the records that you want to use the Lens paradigm on.
@With @Lenses record Person(String firstName, String lastName, House house) { }
@With @Lenses record House(Address address) { }
@With @Lenses record Address(String street, String zipCode, City city) { }
@With @Lenses record City(String name) { }
This will generate a helper class holding Lens
instances that can be applied at will. To enable the method chain for nested lensing it also contains a µ
instance. The name is chosen to make collisions with field names very unlikely.
The library is a work-in-progress. Feel free to try it out - I hope the examples (implemented as unit tests) serve both as documentation and a quick way to experiment. It is published on Maven Central . Currently it is still necessary to also use Lombok’s @With
annotation, this requirement may be dropped in a future release.
Inspiration
The implementation of this library was driven by inspiration from Monocle
, a more powerful library for Scala
authored by Julien Truffaut. Monocle mentions Haskell Lens (authored by Edward Kmett as its own main inspiration. I would like to express gratitude and admiration for their work.
Appendix
Lens Composition
An important property of Lens
(driving its main power) is that you can compose them - meaning you can chain multiple lenses. We have showed how to create a addressCityLens
, capable of transforming the City
of an Address
.
So a Lens
of Person
into City
would be a chain Person -> House -> Address -> City
:
Lens<Person, City> personCityLens = PersonLens.House
.andThen(HouseLens.Address)
.andThen(AddressLens.City);
Interface
To keep the implementation choice of a Lens
record separated from the library API i’ve added an ILens
interface. Providing the two foundational functions, get
and with
, all other behaviour can be implemented. For example, I’ve added get(S s)
and with(S s, T t)
to be able to directly apply a lens onto an instance of S
.
public interface ILens<S, T> {
Function1<S, T> get();
Function2<S, T, S> with();
default T get(S s) {
return get().apply(s);
}
default S with(S subject, T value) {
return with().apply(subject, value);
}
}