import sbt._
resolvers += Resolver.sonatypeRepo("releases")
resolvers += Resolver.sonatypeRepo("snapshots")
val scalaVersion = "2.11.7" // or "2.10.6"
val libraryVersion = "1.2.0-M1" // or "1.3.0-SNAPSHOT"
libraryDependencies ++= Seq(
"com.github.julien-truffaut" %% "monocle-core" % libraryVersion,
"com.github.julien-truffaut" %% "monocle-generic" % libraryVersion,
"com.github.julien-truffaut" %% "monocle-macro" % libraryVersion,
"com.github.julien-truffaut" %% "monocle-state" % libraryVersion,
"com.github.julien-truffaut" %% "monocle-law" % libraryVersion % "test"
)
// for @Lenses macro support
addCompilerPlugin("org.scalamacros" %% "paradise" % "2.0.1" cross CrossVersion.full)
- Motivation
- Lens Creation
- Generic Optics and Instance Location Policy
- Optics Hierarchy
- Modules
- Maintainers and Contributors
- Contact
Monocle is a Lens
library, or more generally an Optics library where Optics gather the concepts
of Lens
, Traversal
, Optional
, Prism
and Iso
. Monocle is strongly inspired by Haskell Lens.
Optics are a set of purely functional abstractions to manipulate (get, set, modify) immutable objects. Optics compose between each other and particularly shine with nested objects.
Scala already provides getters and setters for case classes but modifying nested object is verbose which makes code difficult to understand and reason about. Let's have a look at some examples:
case class Street(name: String, ...) // ... means it contains other fields
case class Address(street: Street, ...)
case class Company(address: Address, ...)
case class Employee(company: Company, ...)
Let's say we have an employee and we need to set the first character of his company street name address in upper case. Here is how we could write it in vanilla Scala:
val employee: Employee = ...
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
}
)
)
)
As you can see copy is not convenient to update nested objects as we need to repeat at each level the full path to reach it. Let's see what could we do with Monocle:
val _name : Lens[Street , String] = ... // we'll see later how to build Lens
val _street : Lens[Address , Street] = ...
val _address: Lens[Company , Address] = ...
val _company: Lens[Employee, Company] = ...
(_company composeLens _address composeLens _street composeLens _name).modify(_.capitalize)(employee)
// you can achieve the same result with less characters using symbolic syntax
(_company ^|-> _address ^|-> _street ^|-> _name).modify(_.capitalize)(employee)
ComposeLens takes two Lens
, one from A to B and another from B to C and creates a third Lens
from A to C.
Therefore, after composing _company, _address, _street and _name, we obtain a Lens
from Employee
to String
(the street name).
In the above example, we used capitalize to upper case the first letter of a String
.
It works but it would be clearer if we could use Lens
to zoom into the first character of a String
.
However, we cannot write such a Lens
because a Lens
defines how to focus from an object S
into a mandatory
object A
and in our case, the first character of a String
is optional as a String
might be empty. For this
we need a sort of partial Lens
, in Monocle it is called Optional
.
import monocle.function.headOption._ // to use headOption (a generic optic)
import monocle.std.string._ // to get String instance for HeadOption
((_company composeLens _address
composeLens _street
composeLens _name
composeOptional headOption).modify(toUpper)(employee)
Similarly to composeLens, composeOptional takes two Optional
, one from A to B and another from B to C and
creates a third Optional
from A to C. All Lens
can be seen as Optional
where the optional element to zoom to is always
present, hence composing an Optional
and a Lens
always produces an Optional
(see class diagram for full inheritance
relation between Optics).
For more examples, see the example
module.
There are 3 ways to create Lens
, each with their pro and cons:
-
The manual method where we construct a
Lens
by passingget
andset
functions:val _company = Lens[Employee, Company](_.company)( c => e => e.copy(company = c)) // or with some type inference val _company = Lens((_: Employee).company)( c => e => e.copy(company = c))
-
The semi-automatic method using the
GenLens
blackbox macro:val _company = GenLens[Employee](_.company) val _name = GenLens[Employee](_.name) // or val genLens = GenLens[Employee] val (_company, _name) = (genLens(_.company) , genLens(_.name))
-
Finally, the fully automatic method using the
@Lenses
macro annotation.@Lenses
generatesLens
for every accessor of a case class in its companion object (even if there is no companion object defined). This solution is the most boiler plate free but it has several disadvantages:- users need to add the macro paradise plugin to their project.
- poor IDE supports, at the moment only IntelliJ recognises the generated
Lens
. - requires access to the case classes since you need to annotate them.
@Lenses case class Employee(company: Company, name: String, ...) // generates Employee.company: Lens[Employee, Company] // and Employee.name : Lens[Employee, String] // you can add a prefix to Lenses constructor @Lenses("_") case class Employee(company: Company, name: String, ...) // generates Employee._company: Lens[Employee, Company]
Note: GenLens
and @Lenses
are both limited to case classes
A generic optic is an optic that is applicable to different types. For example, headOption
is an Optional
from
some type S
to its optional first element of type A
. In order to use headOption
(or any generic optics), you
need to:
- import the generic optic in your scope via
import monocle.function.headOption._
orimport monocle.function._
- have the required instance of the type class
monocle.HeadOption
in your scope, e.g. if you want to useheadOption
from aList[Int]
, you need an instance ofHeadOption[List[Int], Int]
. This instance can be either provided by you or by Monocle.
Monocle defines generic optic instances in the following packages:
monocle.std
for standard Scala library and Scalaz classes, e.g.List, Vector, Map, IList, OneAnd
monocle.generic
for Shapeless classes, e.g.HList, CoProduct
An example shows how to use Monocle imports.
- Core defines the main library concepts: optics, typeclass, syntax. Core only depends on scalaz for type classes.
- Law defines properties for optics using discipline and scalacheck.
- Macro defines a set of macros to generate optics automatically.
- Generic is an experiment to provide highly generalised Optics using shapeless.
The current maintainers (people who can merge pull requests) are:
- Julien Truffaut - @julien-truffaut
- Ilan Godik - @NightRa
- Naoki Aoyama - @aoiroaoino
and the contributors (people who committed to Monocle).
If you have any question, we have a gitter channel and a mailing group