In this post we’ll look at Lens which is a pure functional way of getting and setting data. Before we get into lenses, we’ll look at why we need lenses by looking at a simple example.
Motivating Example
1 | @ case class Address(city: String, zip: Int) |
Say we have a class Person
which has the name of the person and their address where the address is represented by Address
. What we’d like to do is to change the address of the person. Let’s go ahead and create a Person
.
1 | @ val p1 = Person("John Doe", Address("Doe Ville", 7)) |
Changing the Address
while maintaining immutability is fairly easy.
1 | @ val p2 = p1.copy( |
The problem arises when things begin to nest. Let’s create an Order
class representing an order placed by a Person
.
1 | @ case class Order(person: Person, items: List[String]) |
Now, the person would like to change the address to which the items are delivered.
1 | @ val o2 = o1.copy( |
So, the deeper we nest, the uglier it gets. Lenses provide a succinct, functional way to do this.
Lens
Lenses are a way of focusing on a specific part of a deep data structure. Think of them as fancy getters and setters for deep data structures. I’ll begin by demonstrating how we can create and use a lens and then explain the lens laws.
Creating a Lens
1 | @ import scalaz._ |
What we’ve done is create a lens that accepts a Person
object and focuses on its Address
field. lensu
expects two functions - a setter and a getter. In the first function, the setter, we’re making a copy of the Person
object passed to the lens and updating its address field with the new one. In the second function, the getter, we’re simply returning the address field. Lets see this in action by getting and setting values.
Getting a Field
1 | @ addressInPerson.get(p1) |
Once you create a lens, you get a get
method which returns the address field in the Person
object.
Setting a Field
1 | @ val p3 = addressInPerson.set(p1, Address("Bar Town", 10)) |
Similarly, there’s a set
method which lets you set fields to specific values.
Modifying a Field
1 | @ val p4 = addressInPerson.mod({ a => a.copy(city = s"${a.city}, NY") }, p1) |
mod
lets you modify the field. It expects a function that maps Address
to Address
. In the example here, we’re appending “NY” to the name of the city.
Lenses are Composable
The true power of lenses is in composing them. You can compose two lenses together to look deeper into a data structure. For example, we’ll create a lens which lets us access the address field of the person in an Order
. We’ll do this by composing two lenses.
1 | // creating the lens |
Ignore the cmd.
prefix to Order
. That is just an Ammonite REPL quirk to avoid confusing with the Order
trait from Scalaz. Next, we’ll combine the two lenses we have.
1 | @ val addressInOrder = personInOrder >=> addressInPerson |
>=>
is the symbolic alias for andThen
. The way you read what we’ve done is: get the person from the order AND THEN get the address from that person.
This allows you to truly keep your code DRY. Now no matter within which data structure Person
and Address
are, you can reuse that lens to get and set those fields. It’s just a matter of creating another lens or few lenses to access the Person
from a deep data structure.
Similarly there’s also compose
which has a symbolic alias <=<
and works in the other direction. I personally find it easier to use andThen
/ >=>
.
Lens Laws
Get-Put: If you get a value from a data structure and put it back in, the data structure stays unchanged.
Put-Get: If you put a value into a data structure and get it back out, you get the most updated value back.
Put-Put: If you put a value into a data structure and then you put another value in the data structure, it’s as if you only put the second value in.
Lenses that obey all the three laws are called “very well-behaved lenses”. You should always ensure that your lenses obey these rules.
Here’s how Scalaz represents these lens laws:
1 | trait LensLaw { |
identity
is get-put law, retention
is put-get law, and doubleSet
is put-put law.
Lenses and State Monads
Formally, a state monad looks like the following:
1 | S => (S, A) |
Given a state S, it computes the resulting state S by making mutations to the existing state S and produces a resulting A. This is a bit abstract so let’s look at a scenario. Say we have a list of people whose addresses we’d like to update to Fancytown with zip code 3. Let’s do that using lenses.
Creating a State
1 | @ val state = for { |
Here we are creating a state using a for
comprehension. The %=
operator accepts a function which maps an Address
to an Address
. What we get back is a state monad. Now that we have a state monad, let’s use it to update the address.
Updating the State
Next, let’s make person p1
move to Fancytown.
1 | @ state(p1) |
Here we are updating person p1
‘s address. What we get back is a new state S
, p1
but with Fancytown address, and the result A
, the new Address
. state(p1)
is the same as state.apply(p1)
. In short, we’re applying that state to a Person
object.
Conclusion
This brings us to the end the post on lenses. Lenses are a powerful way to get, set, and modify fields in your data structures. The best part about them is that they are reusable and can be composed to form lenses that focus deeper into the data structure.