Lens

A Lens is an optic used to zoom inside a Product, e.g. case class, Tuple, HList or even Map.

Lenses have two type parameters generally called S and A: Lens[S, A] where S represents the Product and A an element inside of S.

Let’s take a simple case class with two fields:

case class Address(streetNumber: Int, streetName: String)

We can create a Lens[Address, Int] which zooms from an Address to its field streetNumber by supplying a pair of functions:

  • get: Address => Int
  • set: Int => Address => Address
import monocle.Lens
val streetNumber = Lens[Address, Int](_.streetNumber)(n => a => a.copy(streetNumber = n))

This case is really straightforward so we automated the generation of Lenses from case classes using a macro:

import monocle.macros.GenLens
val streetNumber = GenLens[Address](_.streetNumber)

Once we have a Lens, we can use the supplied get and set functions (nothing fancy!):

val address = Address(10, "High Street")
// address: Address = Address(streetNumber = 10, streetName = "High Street")

streetNumber.get(address)
// res0: Int = 10
streetNumber.set(5)(address)
// res1: Address = Address(streetNumber = 5, streetName = "High Street")

We can also modify the target of Lens with a function, this is equivalent to call get and then set:

streetNumber.modify(_ + 1)(address)
// res2: Address = Address(streetNumber = 11, streetName = "High Street")

val n = streetNumber.get(address)
// n: Int = 10
streetNumber.set(n + 1)(address)
// res3: Address = Address(streetNumber = 11, streetName = "High Street")

We can push the idea even further, with modifyF we can update the target of a Lens in a context, cf cats.Functor:

def neighbors(n: Int): List[Int] =
  if(n > 0) List(n - 1, n + 1) else List(n + 1)

import cats.implicits._ // to get all Functor instance
streetNumber.modifyF(neighbors)(address)
// res4: List[Address] = List(
//   Address(streetNumber = 9, streetName = "High Street"),
//   Address(streetNumber = 11, streetName = "High Street")
// )
streetNumber.modifyF(neighbors)(Address(135, "High Street"))
// res5: List[Address] = List(
//   Address(streetNumber = 134, streetName = "High Street"),
//   Address(streetNumber = 136, streetName = "High Street")
// )

This would work with any kind of Functor and is especially useful in conjunction with asynchronous APIs, where one has the task to update a deeply nested structure with the result of an asynchronous computation:

import scala.concurrent._
import scala.concurrent.ExecutionContext.Implicits._ // to get global ExecutionContext

def updateNumber(n: Int): Future[Int] = Future.successful(n + 1)
streetNumber.modifyF(updateNumber)(address)
// res6: Future[Address] = Future(Success(Address(11,High Street)))

Most importantly, Lenses compose together allowing to zoom deeper in a data structure

case class Person(name: String, age: Int, address: Address)
val john = Person("John", 20, Address(10, "High Street"))

val address = GenLens[Person](_.address)
(address composeLens streetNumber).get(john)
// res7: Int = 10
(address composeLens streetNumber).set(2)(john)
// res8: Person = Person(
//   name = "John",
//   age = 20,
//   address = Address(streetNumber = 2, streetName = "High Street")
// )

Other Ways of Lens Composition

Is possible to compose few Lenses together by using compose:

GenLens[Person](_.name).set("Mike") compose GenLens[Person](_.age).modify(_ + 1)

Same but with the simplified macro based syntax:

import monocle.macros.syntax.lens._

john.lens(_.name).set("Mike").lens(_.age).modify(_ + 1)

(All Setter like optics offer set and modify methods that returns an EndoFunction (i.e. S => S) which means that we can compose modification using basic function composition.)

Sometimes you need an easy way to update Product type inside Sum type - for that case you can compose Prism with Lens by using some:

import monocle.std.option.some
import monocle.macros.GenLens

case class B(c: Int)
case class A(b: Option[B])

val c = GenLens[B](_.c)
// c: Lens[B, Int] = repl.MdocSession$App$$anon$7@1f1de27d
val b = GenLens[A](_.b)
// b: Lens[A, Option[B]] = repl.MdocSession$App$$anon$8@318a4aab

(b composePrism some composeLens c).getOption(A(Some(B(1))))
// res11: Option[Int] = Some(value = 1)

For more detailed view of the various optics composition see Optics

Lens Generation

Lens creation is rather boiler platy but we developed a few macros to generate them automatically. All macros are defined in a separate module (see modules).

import monocle.macros.GenLens
val age = GenLens[Person](_.age)

GenLens can also be used to generate Lens several level deep:

GenLens[Person](_.address.streetName).set("Iffley Road")(john)
// res12: Person = Person(
//   name = "John",
//   age = 20,
//   address = Address(streetNumber = 10, streetName = "Iffley Road")
// )

For those who want to push Lenses generation even further, we created @Lenses macro annotation which generate Lenses for all fields of a case class. The generated Lenses are in the companion object of the case class:

import monocle.macros.Lenses
@Lenses case class Point(x: Int, y: Int)
val p = Point(5, 3)
Point.x.get(p)
// res13: Int = 5
Point.y.set(0)(p)
// res14: Point = Point(x = 5, y = 0)

You can also add a prefix to @Lenses in order to prefix the generated Lenses:

@Lenses("_") case class PrefixedPoint(x: Int, y: Int)
val p = PrefixedPoint(5, 3)
PrefixedPoint._x.get(p)
// res15: Int = 5

Laws

A Lens must satisfy all properties defined in LensLaws from the core module. You can check the validity of your own Lenses using LensTests from the law module.

In particular, a Lens must respect the getSet law which states that if you get a value A from S and set it back in, the result is an object identical to the original one. A side effect of this law is that set must only update the A it points to, for example it cannot increment a counter or modify another value.

def getSet[S, A](l: Lens[S, A], s: S): Boolean =
  l.set(l.get(s))(s) == s

On the other hand, the setGet law states that if you set a value, you always get the same value back. This law guarantees that set is actually updating a value A inside of S.

def setGet[S, A](l: Lens[S, A], s: S, a: A): Boolean =
  l.get(l.set(a)(s)) == a