FAQ
Which imports are required to use typeclass based optics such as at, each, headOption?
All typeclasses are defined in monocle.function
package, you can import optic individually with
import monocle.function.$TYPE_CLASS.$OPTIC
For example
import monocle.function.At.at
import monocle.function.Cons.{headOption, tailOption}
or you can import all typeclass based optics with
import monocle.function.all._
Here is a complete example
import monocle.function.all._
import monocle.macros.GenLens
case class Foo(s: String, is: List[Int])
val foo = Foo("Hello", List(1,2,3))
val is = GenLens[Foo](_.is)
(is composeOptional headOption).getOption(foo)
// res0: Option[Int] = Some(value = 1)
Note: if you use a version of monocle before 1.4.x, you need another import to get the typeclass instance
import monocle.std.list._
What is the difference between at and index? When should I use one or the other?
Both at
and index
define indexed optics. However, at
is a Lens
and index
is an Optional
which means
at
is stronger than index
. Let’s take the example of a Map
import monocle.Iso
val m = Map("one" -> 1, "two" -> 2)
val root = Iso.id[Map[String, Int]]
(root composeOptional index("two")).set(0)(m) // update value at index "two"
// res1: Map[String, Int] = Map("one" -> 1, "two" -> 0) // update value at index "two"
(root composeOptional index("three")).set(3)(m) // noop because m doesn't have a value at "three"
// res2: Map[String, Int] = Map("one" -> 1, "two" -> 2) // noop because m doesn't have a value at "three"
(root composeLens at("three")).set(Some(3))(m) // insert element at "three"
// res3: Map[String, Int] = Map("one" -> 1, "two" -> 2, "three" -> 3) // insert element at "three"
(root composeLens at("two")).set(None)(m) // delete element at "two"
// res4: Map[String, Int] = Map("one" -> 1) // delete element at "two"
(root composeLens at("two")).set(Some(0))(m) // upsert element at "two"
// res5: Map[String, Int] = Map("one" -> 1, "two" -> 0)
In other words, index
can update any existing values while at
can also insert
and delete
.
Since index
is weaker than at
, we can implement an instance of Index
on more data structure than At
.
For instance, List
or Vector
only have an instance of Index
because there is no way to insert an element at an
arbitrary index of a sequence.
Note: root
is a trick to help type inference. Without it, we would get the following error
index("two").set(0)(m)
// error: ambiguous implicit values:
// both method listMapIndex in object Index of type [K, V]monocle.function.Index[scala.collection.immutable.ListMap[K,V],K,V]
// and method mapIndex in object Index of type [K, V]monocle.function.Index[Map[K,V],K,V]
// match expected type monocle.function.Index[S,String,A]
// index("two").set(0)(m)
// ^^^^^^^^^^^^
The problem is that the compiler does not have enough information to infer the correct Index
instance. By using
Iso.id[Map[String, Int]]
as a prefix, we give a hint to the type inference saying we focus on a Map[String, Int]
.
Similarly, if the Map
was in a case class, a Lens
would provide the same kind of hint than Iso.id
case class Bar(kv: Map[String, Int])
(GenLens[Bar](_.kv) composeOptional index("two")).set(0)(Bar(m))
// res7: Bar = Bar(kv = Map("one" -> 1, "two" -> 0))