diff --git a/core/src/main/scala/cats/Traverse.scala b/core/src/main/scala/cats/Traverse.scala index 24015db6b96..0565b0cffb5 100644 --- a/core/src/main/scala/cats/Traverse.scala +++ b/core/src/main/scala/cats/Traverse.scala @@ -1,5 +1,7 @@ package cats +import cats.data.State + import simulacrum.typeclass /** @@ -32,6 +34,17 @@ import simulacrum.typeclass */ def traverse[G[_]: Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]] + /** + * Traverse with a function returning state. + * + * Given a function which returns state, thread the state through + * the running of this function on all the values in F, returning + * F[B] in the state context. + */ + def stateTraverse[S, A, B](fa: F[A])(f: A => State[S, B]): State[S, F[B]] = + State[S, F[B]](s => + traverse[State[S, ?] , A, B](fa)(f).run(s).value) + /** * A traverse followed by flattening the inner result. * @@ -97,4 +110,14 @@ import simulacrum.typeclass override def map[A, B](fa: F[A])(f: A => B): F[B] = traverse[Id, A, B](fa)(f) + + /** + * Traverses through the structure F, pairing the values with + * assigned indices. + * + * The behavior is consistent with the Scala collection library's + * `zipWithIndex` for collections such as `List`. + */ + def indexed[A](fa: F[A]): F[(A, Int)] = + stateTraverse(fa)(a => State((s: Int) => (s + 1, (a, s)))).runA(0).value } diff --git a/tests/src/test/scala/cats/tests/ListTests.scala b/tests/src/test/scala/cats/tests/ListTests.scala index 308e0672789..ffa5c342c09 100644 --- a/tests/src/test/scala/cats/tests/ListTests.scala +++ b/tests/src/test/scala/cats/tests/ListTests.scala @@ -42,4 +42,10 @@ class ListTests extends CatsSuite { l.show should === (l.toString) } } + + test("indexed consistent with zipWithIndex") { + forAll { (fa: List[String]) => + Traverse[List].indexed(fa) should === (fa.zipWithIndex) + } + } }