Skip to main content
Version: 2.x

Composing Optics

One of the great features of optics is that they compose.

If we have an optic that accesses a part Piece of a larger structure Whole and another optic that accesses a part Piece2 of Piece we can combine them to create an optic that accesses the part Piece2 of the original structure Whole. We do this using the >>> operator or its named alias andThen.

For example, say we have an Either[String, Person] representing either a person or a failure message indicating why the person does not exist. We would like to modify the age of the person, if it exists.

Rather than having to define a new optic for this, we can combine the existing right and age optics. The right optic will first "zoom in" to the Person on the right side of the either and then the age optic will further zoom in to the age field within the person.

import zio._
import zio.optics._

case class Person(name: String, age: Int)

object Person {
val age: Lens[Person, Int] =
Lens(
person => Right(person.age),
age => person => Right(person.copy(age = age))
)
}

val optic: Optional[Either[String, Person], Int] =
Optic.right >>> Person.age

The >>> operator will automatically take care of composing the optics together for us, returning the most specific optic possible given the two optics that we are combining.

Sometimes instead of directly accessing part of the value accessed by the first optic like this we would like to access each value within a collection accessed by the first optic.

For example, if we have an optic that returns a Chunk[Person] we may want to modify a field in every Person returned by the first optic instead of modifying the Chunk[Person] itself. We can do this using the foreach operator.

def hasJanuaryBirthday(person: Person): Boolean =
???

val januaryAges: Traversal[Chunk[Person], Int] =
Optic.filter(hasJanuaryBirthday).foreach(Person.age)