Traverse
API Documentation: Traverse
In the Applicative tutorial we saw a more polymorphic version of the standard library
Future.traverse and Future.sequence functions, generalizing Future to be any
F[_] that's Applicative.
import cats.Applicative
def traverse[F[_]: Applicative, A, B](as: List[A])(f: A => F[B]): F[List[B]] =
as.foldRight(Applicative[F].pure(List.empty[B])) { (a: A, acc: F[List[B]]) =>
val fb: F[B] = f(a)
Applicative[F].map2(fb, acc)(_ :: _)
}
Here traverse still has knowledge of List, but we could just as easily use
Vector or some similar data type. Another example is a binary tree:
object tree {
sealed abstract class Tree[A] extends Product with Serializable {
def traverse[F[_]: Applicative, B](f: A => F[B]): F[Tree[B]] = this match {
case Tree.Empty() => Applicative[F].pure(Tree.Empty())
case Tree.Branch(v, l, r) => Applicative[F].map3(f(v), l.traverse(f), r.traverse(f))(Tree.Branch(_, _, _))
}
}
object Tree {
final case class Empty[A]() extends Tree[A]
final case class Branch[A](value: A, left: Tree[A], right: Tree[A]) extends Tree[A]
}
}
import tree._
This suggests an abstraction over "things that can be traversed over," hence Traverse.
trait Traverse[F[_]] {
def traverse[G[_]: Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]]
}
// Example implementation for List
implicit val traverseForList: Traverse[List] = new Traverse[List] {
def traverse[G[_]: Applicative, A, B](fa: List[A])(f: A => G[B]): G[List[B]] =
fa.foldRight(Applicative[G].pure(List.empty[B])) { (a, acc) =>
Applicative[G].map2(f(a), acc)(_ :: _)
}
}
// Example implementation for Tree
implicit val traverseForTree: Traverse[Tree] = new Traverse[Tree] {
def traverse[G[_]: Applicative, A, B](fa: Tree[A])(f: A => G[B]): G[Tree[B]] =
fa.traverse(f)
}
A note on sequencing
Sometimes you will be given a traversable that has effectful values already, such as
a List[Option[A]]. Since the values themselves are effects, traversing with identity
will turn the traversable "inside out."
import cats.syntax.all._
val list = List(Some(1), Some(2), None)
// list: List[Option[Int]] = List(Some(value = 1), Some(value = 2), None)
val traversed = list.traverse(identity)
// traversed: Option[List[Int]] = None
Cats provides a convenience method for this called sequence.
val sequenced = list.sequence
// sequenced: Option[List[Int]] = None
In general t.map(f).sequence can be replaced with t.traverse(f).
Traversables are Functors
As it turns out every Traverse is a lawful Functor. By carefully picking the G to
use in traverse we can implement map.
First let's look at the two signatures.
import cats.{Applicative, Traverse}
def traverse[F[_]: Traverse, G[_]: Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]] = ???
def map[F[_]: Traverse, A, B](fa: F[A])(f: A => B): F[B] = ???
Both have an F[A] parameter and a similar f parameter. traverse expects the return type
of f to be G[B] whereas map just wants B. Similarly the return type of traverse is
G[F[B]] whereas for map it's just F[B]. This suggests we need to pick a G such that
G[A] communicates exactly as much information as A. We can conjure one up by simply wrapping
an A.
final case class Id[A](value: A)
In order to call traverse Id needs to be Applicative which is straightforward - note that while
Id just wraps an A, it is still a type constructor which matches the shape required by Applicative.
implicit val applicativeForId: Applicative[Id] = new Applicative[Id] {
def ap[A, B](ff: Id[A => B])(fa: Id[A]): Id[B] = Id(ff.value(fa.value))
def pure[A](a: A): Id[A] = Id(a)
}
Now we can implement map by wrapping and unwrapping Id as necessary.
import cats.Functor
trait Traverse[F[_]] extends Functor[F] {
def traverse[G[_]: Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]]
def map[A, B](fa: F[A])(f: A => B): F[B] =
traverse(fa)(a => Id(f(a))).value
}
Id is provided in Cats as a type alias type Id[A] = A.
Traversables are Foldable
The Foldable type class abstracts over "things that can be folded over" similar to how
Traverse abstracts over "things that can be traversed." It turns out Traverse is strictly
more powerful than Foldable - that is, foldLeft and foldRight can be implemented
in terms of traverse by picking the right Applicative. However, cats.Traverse does not
implement foldLeft and foldRight as the actual implementation tends to be ineffecient.
For brevity and demonstration purposes we'll implement the equivalent foldMap method in terms
of traverse by using cats.data.Const. You can then implement foldRight in terms of foldMap,
and foldLeft can then be implemented in terms of foldRight, though the resulting implementations
may be slow.
import cats.{Applicative, Monoid, Traverse}
import cats.data.Const
def foldMap[F[_]: Traverse, A, B: Monoid](fa: F[A])(f: A => B): B =
Traverse[F].traverse[Const[B, *], A, B](fa)(a => Const(f(a))).getConst
This works because Const[B, *] is an Applicative if B is a Monoid, as explained in the documentation of Const.
Further Reading
- The Essence of the Iterator Pattern - Gibbons, Oliveira. JFP 2009.