Traversal

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: monocle.Traversal[List[Int],Int] = monocle.PTraversal$$anon$5@1c06784
eachL.set(0)(xs) should be(res0)

eachL.modify(_ + 1)(xs) should be(res1)

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

eachL.getAll(xs) should be(res0)

eachL.headOption(xs) should be(res1)

eachL.find(_ > 3)(xs) should be(res2)

eachL.all(_ % 2 == 0)(xs) should be(res3)

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)) should be(res0)

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: monocle.Traversal[Map[Int,String],String] = $anon$1@5fadf001

filterEven.modify(_.toUpperCase)(m) should be(res0)

Law

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

Another important law is composeModify also known as fusion law:

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)

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)

modifyGetAll(eachL, List(1, 2, 3), (x: Int) => x + 1) should be(res0)

composeModify(eachL, List(1, 2, 3), (x: Int) => x + 1, (y: Int) => y + 2) should be(res1)