Optics are a group of purely functional abstractions to manipulate (
modify, …) immutable objects.
Monocle is published to Maven Central and cross-built for Scala
2.13 so you can just add the following to your build:
val monocleVersion = "2.0.0" // depends on cats 2.x libraryDependencies ++= Seq( "com.github.julien-truffaut" %% "monocle-core" % monocleVersion, "com.github.julien-truffaut" %% "monocle-macro" % monocleVersion, "com.github.julien-truffaut" %% "monocle-law" % monocleVersion % "test" )
If you want to use macro annotations such as
@Lenses, you will also need to include:
addCompilerPlugin("org.scalamacros" %% "paradise" % "2.1.1" cross CrossVersion.full)
Scala already provides getters and setters for case classes but modifying nested objects is verbose which makes code difficult to understand and reason about. Let’s have a look at some examples:
case class Street(number: Int, name: String) case class Address(city: String, street: Street) case class Company(name: String, address: Address) case class Employee(name: String, company: Company)
Let’s say we have an employee and we need to upper case the first character of his company street name. Here is how we could write it in vanilla Scala:
val employee = Employee("john", Company("awesome inc", Address("london", Street(23, "high street"))))
employee.copy( company = employee.company.copy( address = employee.company.address.copy( street = employee.company.address.street.copy( name = employee.company.address.street.name.capitalize // luckily capitalize exists ) ) ) ) // res0: Employee = Employee(john,Company(awesome inc,Address(london,Street(23,High street))))
As we can see
copy is not convenient to update nested objects because we need to repeat ourselves.
Let’s see what could we do with Monocle (type annotations are only added for clarity):
import monocle.Lens import monocle.macros.GenLens val company : Lens[Employee, Company] = GenLens[Employee](_.company) val address : Lens[Company , Address] = GenLens[Company](_.address) val street : Lens[Address , Street] = GenLens[Address](_.street) val streetName: Lens[Street , String] = GenLens[Street](_.name) company composeLens address composeLens street composeLens streetName
composeLens takes two
Lenses, one from
B and another one from
C and creates a third
Therefore, after composing
name, we obtain a
String (the street name).
Now we can use this
Lens issued from the composition to
modify the street name using the function
(company composeLens address composeLens street composeLens streetName).modify(_.capitalize)(employee) // res2: Employee = Employee(john,Company(awesome inc,Address(london,Street(23,High street))))
modify lifts a function
String => String to a function
Employee => Employee.
It works but it would be clearer if we could zoom into the first character of a
String with a
However, we cannot write such a
Lenses require the field they are directed at to be mandatory.
In our case the first character of a
String is optional as a
String can be empty.
So we need another abstraction that would be a sort of partial
Lens, in Monocle it is called an
import monocle.function.Cons.headOption // to use headOption (an optic from Cons typeclass)
(company composeLens address composeLens street composeLens streetName composeOptional headOption).modify(_.toUpper)(employee) // res3: Employee = Employee(john,Company(awesome inc,Address(london,Street(23,High street))))
composeOptional takes two
Optionals, one from
B and another from
creates a third
Lenses can be seen as
Optionals where the optional element to zoom into is always
present, hence composing an
Optional and a
Lens always produces an
Optional (see class diagram for full inheritance
relation between optics).
Monocle offers various functions and macros to cut the boilerplate even further, here is an example:
import monocle.macros.syntax.lens._ // import monocle.macros.syntax.lens._ employee.lens(_.company.address.street.name).composeOptional(headOption).modify(_.toUpper) // res4: Employee = Employee(john,Company(awesome inc,Address(london,Street(23,High street))))
Maintainers and contributors
Monocle is available thanks to its maintainers (people who can merge pull requests):
- Julien Truffaut - @julien-truffaut
- Ilan Godik - @NightRa
- Naoki Aoyama - @aoiroaoino
- Kenji Yoshida - @xuwei-k
- Ken Scambler - @kenbot
and its contributors (people who have pushed commits to Monocle).
Copyright and license
Copyright the maintainers, 2016.