IndexedTraversal
An IndexedTraversal is an optic used to focus on zero, one, or many values and their indices.
IndexedTraversal is usually used for collections like List, Map, Array.
IndexedTraversal internal encoding
Polymorphic IndexedTraversal
IndexedTraversal_[I, S, T, A, B]
IndexedTraversal_[I, S, T, A, B] is a function Indexed[P, I, A, B] => P[S, T] that takes a Wander of P[_, _].
/**
* @tparam I the index of an IndexedTraversal_
* @tparam S the source of an IndexedTraversal_
* @tparam T the modified source of an IndexedTraversal_
* @tparam A the foci of an IndexedTraversal_
* @tparam B the modified foci of an IndexedTraversal_
*/
abstract class IndexedTraversal_[I, S, T, A, B] {
def apply[P[_, _]](indexed: Indexed[P, I, A, B])(implicit ev: Wander[P]): P[S, T]
}
IndexedTraversal_[I, S, T, A, B] changes its foci from A to B, resulting in a change of structure from S to T.
A IndexedTraversal that changes its foci/structure, is called Polymorphic IndexedTraversal.
Monomorphic IndexedTraversal
IndexedTraversal[I, S, A]
IndexedTraversal[I, S, A] is a type alias for IndexedTraversal_[I, S, S, A, A], which has the same type of foci A, thus preserving the same type of structure S.
type IndexedTraversal[I, S, A] = IndexedTraversal_[I, S, S, A, A]
IndexedTraversal[I, S, A] means that the type S might contain zero, one, or many values of type A, and the I is the index or the location of the focus.
An IndexedTraversal that does not change its foci/structure, is called Monomorphic IndexedTraversal.
Constructing IndexedTraversals
IndexedTraversal_[I, S, T, A, B] is constructed using the IndexedTraversal_[I, S, T, A, B]#apply function.
For a given IndexedTraversal_[I, S, T, A, B] it takes two functions as arguments,
view: S => (A, I) which is a getter function, that produces zero, one, or many elements of A, each tupled with its index I given an S,
and set: S => B => T function which takes a structure S and a new focus B and returns
a structure of T filled will all foci of that B.
object IndexedTraversal_ {
def apply[I, S, T, A, B](get: S => (A, I))(set: S => B => T): IndexedTraversal_[I, S, T, A, B]
}
IndexedTraversal[I, S, A] is constructed using the IndexedTraversal[I, S, A]#apply function.
For a given IndexedTraversal[S, A] it takes two functions as arguments, view: S => (A, I) which is a getter function, that produces zero, one, or many elements of A, each tupled with its index I given an S,
and set: S => A => S function which takes a structure S and a focus A and returns a
new structure S filled will all foci of that A.
object IndexedTraversal {
def apply[S, A](get: S => (A, I))(set: S => A => S): IndexedTraversal[I, S, A]
}
Most of the time we will be dealing with collections. We can use the fromTraverse and fromTraverseWithIndex methods.
This is how we can create an IndexedTraversal for a Traverse type, which has an Int index:
import cats.syntax.option._
// import cats.syntax.option._ // (none, some) functions
import cats.syntax.eq._ // triple equals (===)
// import cats.syntax.eq._
import proptics.{IndexedTraversal, IndexedTraversal_}
// import proptics.IndexedTraversal
val list: List[Int] = List.range(1, 5)
// list: List[Int] = List(1, 2, 3, 4)))
val optionByPredicate: ((Int, Int)) => Option[Int] =
{ case (i, a) => if (i % 2 === 0) a.some else none[Int] }
// optionByPredicate: ((Int, Int)) => Option[Int] = $Lambda$10219/0x0000000802b8d040@e88bac3
val fromTraverse: IndexedTraversal[Int, List[Int], Int] =
IndexedTraversal.fromTraverse[List, Int]
// fromTraverse: proptics.IndexedTraversal[Int,List[Int],Int] =
// proptics.IndexedTraversal_$$anon$8@2245a36a
This is how we can create an IndexedTraversal for a TraverseWithIndex type:
import proptics.instances.traverseWithIndex._
// import proptics.instances.traverseWithIndex._
import cats.syntax.eq._
// import cats.syntax.eq._
import cats.syntax.semigroup._
// import cats.syntax.semigroup._
val emptyMap = Map.empty[String, List[String]]
// emptyMap: scala.collection.immutable.Map[String,List[String]] = Map()
val seriesMap: Map[String, List[String]] = Map[String, List[String]](
"tt0903747" -> List("True Detective", "Fargo", "Dexter"),
"tt2356777" -> List("Breaking Bad", "Fargo", "Dexter"),
"tt2802850" -> List("Breaking Bad", "True Detective", "Dexter"),
"tt0773262" -> List("Breaking Bad", "True Detective", "Fargo")
)
// seriesMap: Map[String,List[String]] =
// Map(tt0903747 -> List(True Detective, Fargo, Dexter),
// tt2356777 -> List(Breaking Bad, Fargo, Dexter),
// tt2802850 -> List(Breaking Bad, True Detective, Dexter),
// tt0773262 -> List(Breaking Bad, True Detective, Fargo))
val mapIndexedTraverse =
IndexedTraversal.fromTraverseWithIndex[Map[String, *], String, List[String]]
// mapIndexedTraverse:
// proptics.IndexedTraversal[String,immutable.Map[String,List[String]],List[String]] =
// proptics.IndexedTraversal_$$anon$28@58891f02
def bbIsFirst(list: List[String]): Boolean = list.headOption.exists(_ === "Breaking Bad")
// bbIsFirst(list: List[String]): Boolean
Common functions of a IndexedTraversal
viewAll
listIndexedTraversal.viewAll(list)
// res0: List[(Int, Int)] = List((0,1), (1,2), (2,3), (3,4))
mapIndexedTraverse.viewAll(seriesMap)
// res1: List[(List[String], String)] =
// List((List(True Detective, Fargo, Dexter),tt0903747),
// (List(Breaking Bad, Fargo, Dexter),tt2356777),
// (List(Breaking Bad, True Detective, Dexter),tt2802850),
// (List(Breaking Bad, True Detective, Fargo),tt0773262))
set
listIndexedTraversal.set(list)
// res2: List[Int] = List(9, 9, 9, 9)
mapIndexedTraverse.set(List.empty)(seriesMap)
// res3: scala.collection.immutable.Map[String,List[String]] =
// Map(tt0903747 -> List(),
// tt2356777 -> List(),
// tt2802850 -> List(),
// tt0773262 -> List())
preview/first
listIndexedTraversal.set(list)
// res4: Option[(Int, Int)] = Some((0,1))
// synonym for preview
listIndexedTraversal.first(list)
// res5: Option[(Int, Int)] = Some((0,1))
mapIndexedTraverse.preview(list)
// res6: Option[(List[String], String)] =
// Some((List(True Detective, Fargo, Dexter),tt0903747))
over
listIndexedTraversal.over(_._2 + 1)(list)
// res7: List[Int] = List(2, 3, 4, 5)
mapIndexedTraverse.over { case (ls, _) => ls.take(1) }(seriesMap)
// res8: scala.collection.immutable.Map[String,List[String]] =
// Map(tt0903747 -> List(True Detective),
// tt2356777 -> List(Breaking Bad),
// tt2802850 -> List(Breaking Bad),
// tt0773262 -> List(Breaking Bad))
traverse
import cats.instances.option._ // summons an Applicative[Option] instance
// import cats.instances.option._
listIndexedTraversal.traverse(list)(optionByPredicate)
// res9: Option[List[Int]] = None
listIndexedTraversal.traverse(list)(_._2.some)
// res10: Option[List[Int]] = Some(List(1, 2, 3, 4))
mapIndexedTraverse.traverse(seriesMap) {
case (ls, _) => ls.headOption.map(List(_))
}
// res11: Option[scala.collection.immutable.Map[String,List[String]]] =
// Some(Map(tt0903747 -> List(True Detective),
// tt2356777 -> List(Breaking Bad),
// tt2802850 -> List(Breaking Bad),
// tt0773262 -> List(Breaking Bad)))
mapIndexedTraverse.traverse[Option](seriesMap) { case (ls, _) =>
ls.headOption.filter(_ === "Breaking Bad").map(List(_))
}
// res12: Option[scala.collection.immutable.Map[String,List[String]]] = None
foldMap
import cats.instances.int._ // summons a Semigroup[Int] instance
// import cats.instances.int._
import cats.instances.option._ // summons a Monoid[Option[Int]] instance
// import cats.instances.option._
listIndexedTraversal.foldMap(list)(optionByPredicate)
// res13: Option[Int] = Some(4)
mapIndexedTraverse.foldMap[Map[String, List[String]]](seriesMap) { case (ls, k) =>
if (bbIsFirst(ls)) Map(k -> ls) else emptyMap
}
// res14: Map[String,List[String]] =
// Map(tt0773262 -> List(Breaking Bad, True Detective, Fargo),
// tt2802850 -> List(Breaking Bad, True Detective, Dexter),
// tt2356777 -> List(Breaking Bad, Fargo, Dexter))
foldRight
listIndexedTraversal.foldRight(list)(List.empty[(Int, Int)])(_ :: _)
// res15: List[(Int, Int)] = List((0,1), (1,2), (2,3), (3,4))
mapIndexedTraverse.foldRight[Map[String, List[String]]](seriesMap)(emptyMap) { case ((ls, k), map) =>
(if (bbIsFirst(ls)) Map(k -> ls) else emptyMap) |+| map
}
// res16: Map[String,List[String]] =
// Map(tt0773262 -> List(Breaking Bad, True Detective, Fargo),
// tt2802850 -> List(Breaking Bad, True Detective, Dexter),
// tt2356777 -> List(Breaking Bad, Fargo, Dexter))
foldLeft
listIndexedTraversal.foldLeft(list)(List.empty[(Int, Int)])((xs, x) => x :: xs)
// res17: List[(Int, Int)] = List((3,4), (2,3), (1,2), (0,1))
mapIndexedTraverse.foldLeft[Map[String, List[String]]](seriesMap)(emptyMap) { case (map, (ls, k)) =>
(if (bbIsFirst(ls)) Map(k -> ls) else emptyMap) |+| map
}
// res18: Map[String,List[String]] =
// Map(tt2356777 -> List(Breaking Bad, Fargo, Dexter,
// tt2802850 -> List(Breaking Bad, True Detective, Dexter),
// tt0773262 -> List(Breaking Bad, True Detective, Fargo))
forall
listIndexedFold.forall(_._2 < 9)(list)
// res19: Boolean = true
mapIndexedTraverse.forall(pair => bbIsFirst(pair._1))(seriesMap)
// res20: Boolean = false
exists
listIndexedTraversal.exists(_._2 < 9)(list)
// res21: Boolean = true
mapIndexedTraverse.exists(pair => bbIsFirst(pair._1))(seriesMap)
// res22: Boolean = true
contains
listIndexedTraversal.contains((0, 9))(list)
// res23: Boolean = false
listIndexedTraversal.contains((1, 2))(list)
// res24: Boolean = true
mapIndexedTraverse.contains((List("Fargo"), "tt0773262"))(seriesMap)
// res25: Boolean = false
isEmpty
listIndexedTraversal.isEmpty(list)
// res26: Boolean = false
mapIndexedTraverse.isEmpty(seriesMap)
// res27: Boolean = false
find
listIndexedTraversal.find(_._2 === 9)(list)
// res28: Option[Int] = None
mapIndexedTraverse.find(pair => bbIsFirst(pair._1))(seriesMap)
// res29: Option[(List[String], String)] = Some((List(Breaking Bad, True Detective, Fargo),tt0773262))
last
listIndexedTraversal.last(list)
// res30: Option[Int] = Some((3, 4))
mapIndexedTraverse.last(seriesMap)
// res31: Option[(List[String], String)] =
// Some((List(Breaking Bad, True Detective, Fargo),tt0773262))
minimum
listIndexedTraversal.minimum(list)
// res32: Option[Int] = Some(1)
maximum
listIndexedTraversal.maximum(list)
// res33: Option[Int] = Some(4)
Syntax
We can take advantage of the syntax package for IndexedTraversal.
If we create a polymorphic IndexedTraversal_[I, F[G[A]], F[A], G[A], A] such that the source would be a nested structure of F[G[A]] and the initial foci G[A] would have an Applicative[G] instance, and the modified foci would be an A , then we could
flip types F[_] and G[_] from F[G[A]] to G[F[A]] using the sequence method.
Consider F[_] to be a List, G[_] to be an Option and A to be an Int, the IndexedTraversal would be:
IndexedTraversal_[Int, List[Option[Int]], List[Int], Option[Int], Int]
import cats.syntax.eq._
// import cats.syntax.eq._
import cats.syntax.option._
// import cats.syntax.option._
import cats.instances.list._
// import cats.instances.list._
import proptics.IndexedTraversal_
// import proptics.IndexedTraversal_
import proptics.syntax.indexedTraversal._
// import proptics.syntax.indexedTraversal._
val list: List[(Int, Int)] = List.range(1, 5).zipWithIndex.map(_.swap)
// list: List[(Int, Int)] = List((0,1), (1,2), (2,3), (3,4))
val optionByPredicate: ((Int, Int)) => (Int, Option[Int]) = {
case (i, a) => if (i % 2 === 0) (i, a.some) else (i, none[Int])
}
// optionByPredicate: ((Int, Int)) => (Int, Option[Int]) = $Lambda$11359/0x00000008026ce840@5eeb22a9
val listOfOptions = list.map(optionByPredicate)
// listOfOptions: List[Option[(Int, Int)]] = List(Some((0,1)), None, Some((2,3)), None)
val listOfSomeOptions: List[(Int, Option[Int])] = list.map { case (i, a) => (i, a.some) }
// listOfSomeOptions: List[(Int, Option[Int])] =
// List((0,Some(1)), (1,Some(2)), (2,Some(3)), (3,Some(4)))
val indexedTraversal: IndexedTraversal_[Int, List[(Int, Option[Int])], List[Int], Option[Int], Int] =
IndexedTraversal_.fromTraverse[List, Int, Option[Int], Int]
// indexedTraversal: IndexedTraversal_[Int,List[(Int, Option[Int])],List[Int],Option[Int],Int] =
// proptics.IndexedTraversal_$$anon$8@50ebce6a
sequence
indexedTraversal.sequence(listOfOptions)
// res0: Option[List[Int]] = None
indexedTraversal.sequence(listOfSomeOptions)
// res1: Option[List[Int]] = Some(List(1, 2, 3, 4))
Laws
An IndexedTraversal must satisfy all IndexedTraversalLaws. These laws reside in the <a href="../../api/proptics/law/>proptics.law package.
import cats.instances.list._
// import cats.instances.list._
import cats.syntax.eq._
// import cats.syntax.eq._
import cats.{Applicative, Eq}
// import cats.{Applicative, Eq}
import proptics.IndexedTraversal
// import proptics.IndexedTraversal
val nel = NonEmptyList.fromListUnsafe(List(1, 2, 3, 4, 5, 6))
// nel: cats.data.NonEmptyList[Int] = NonEmptyList(1, 2, 3, 4, 5, 6)
val headIndexedTraversal: IndexedTraversal[Int, NonEmptyList[Int], Int] =
IndexedTraversal[Int, NonEmptyList[Int], Int](nel => (0, nel.head))(nel => i => nel.copy(head = i))
// headIndexedTraversal: proptics.IndexedTraversal[Int,cats.data.NonEmptyList[Int],Int] =
// proptics.IndexedTraversal_$$anon$9@1e81167a
Traversing with "empty" handler shouldn't change anything
def respectPurity[F[_]: Applicative, I, S, A](indexedTraversal: IndexedTraversal[I, S, A], s: S)
(implicit ev: Eq[F[S]]): Boolean =
indexedTraversal.traverse[F](s) { case (a, _) => Applicative[F].pure(a) } === Applicative[F].pure(s)
respectPurity[Id, Int, NonEmptyList[Int], Int](headIndexedTraversal, nel)
// res0: Boolean = true
Running twice with different handlers is equivalent to running it once with the composition of those handlers
def consistentFoci[I, S: Eq, A](s: S, f: (A, I) => A, g: (A, I) => A, indexedTraversal: IndexedTraversal[I, S, A]): Boolean =
(indexedTraversal.over(f.tupled) compose indexedTraversal.over(g.tupled))(s) ===
indexedTraversal.over { case (a, i) => f(g(a, i), i) }(s)
consistentFoci[Int, NonEmptyList[Int], Int](nel, (a, _) => a + 1, (a, _) => a * 2, headIndexedTraversal)
// res1: Boolean = true