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))