Traversal

A Traversal is the generalisation of an Optional to several targets. In other word, a Traversal allows to focus from a type S into 0 to n values of type A.

The most common example of a Traversal would be to focus into all elements inside of a container (e.g. List, Vector, Option). To do this we will use the relation between the typeclass cats.Traverse and Traversal:

import monocle.Traversal
import cats.implicits._   // to get all cats instances including Traverse[List]

val xs = List(1,2,3,4,5)
val eachL = Traversal.fromTraverse[List, Int]
// eachL: Traversal[List[Int], Int] = monocle.PTraversal$$anon$5@49e6f081
eachL.set(0)(xs)
// res0: List[Int] = List(0, 0, 0, 0, 0)
eachL.modify(_ + 1)(xs)
// res1: List[Int] = List(2, 3, 4, 5, 6)

A Traversal is also a Fold, so we have access to a few interesting methods to query our data:

eachL.getAll(xs)
// res2: List[Int] = List(1, 2, 3, 4, 5)
eachL.headOption(xs)
// res3: Option[Int] = Some(value = 1)
eachL.find(_ > 3)(xs)
// res4: Option[Int] = Some(value = 4)
eachL.all(_ % 2 == 0)(xs)
// res5: Boolean = false

Traversal also offers smart constructors to build a Traversal for a fixed number of target (currently 2 to 6 targets):

case class Point(id: String, x: Int, y: Int)

val points = Traversal.apply2[Point, Int](_.x, _.y)((x, y, p) => p.copy(x = x, y = y))
points.set(5)(Point("bottom-left",0,0))
// res6: Point = Point(id = "bottom-left", x = 5, y = 5)

Finally, if you want to build something more custom you will have to implement a Traversal manually. A Traversal is defined by a single method modifyF which corresponds to the Van Laarhoven representation.

For example, let’s write a Traversal for Map that will focus into all values where the key satisfies a certain predicate:

import monocle.Traversal
import cats.Applicative
import alleycats.std.map._ // to get Traverse instance for Map (SortedMap does not require this import)

def filterKey[K, V](predicate: K => Boolean): Traversal[Map[K, V], V] =
    new Traversal[Map[K, V], V]{
      def modifyF[F[_]: Applicative](f: V => F[V])(s: Map[K, V]): F[Map[K, V]] =
        s.map{ case (k, v) =>
          k -> (if(predicate(k)) f(v) else v.pure[F])
        }.sequence
    }

val m = Map(1 -> "one", 2 -> "two", 3 -> "three", 4 -> "Four")
val filterEven = filterKey[Int, String](_ % 2 == 0)
// filterEven: Traversal[Map[Int, String], String] = repl.MdocSession$App$$anon$1@607f571b

filterEven.modify(_.toUpperCase)(m)
// res7: Map[Int, String] = Map(
//   4 -> "FOUR",
//   3 -> "three",
//   2 -> "TWO",
//   1 -> "one"
// )

Laws

A Traversal must satisfy all properties defined in TraversalLaws from the core module. You can check the validity of your own Traversal using TraversalTests from the law module.

In particular, a Traversal must respect the modifyGetAll law which checks that you can modify all elements targeted by a Traversal

def modifyGetAll[S, A](t: Traversal[S, A], s: S, f: A => A): Boolean =
    t.getAll(t.modify(f)(s)) == t.getAll(s).map(f)

Another important law is composeModify also known as fusion law:

def composeModify[S, A](t: Traversal[S, A], s: S, f: A => A, g: A => A): Boolean =
    t.modify(g)(t.modify(f)(s)) == t.modify(g compose f)(s)