diff --git a/core/src/main/scala/cats/Traverse.scala b/core/src/main/scala/cats/Traverse.scala index 24015db6b96..f7fb664f2e7 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 /** @@ -97,4 +99,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)] = + traverse(fa)(a => State((s: Int) => (s + 1, (a, s)))).runA(0).value } diff --git a/tests/src/test/scala/cats/tests/TraverseTests.scala b/tests/src/test/scala/cats/tests/TraverseTests.scala new file mode 100644 index 00000000000..f98f4a83eaa --- /dev/null +++ b/tests/src/test/scala/cats/tests/TraverseTests.scala @@ -0,0 +1,25 @@ +package cats +package tests + +class TraverseTestsAdditional extends CatsSuite { + + def checkIndexedStackSafety[F[_]](fromRange: Range => F[Int])(implicit F: Traverse[F]): Unit = { + val res = F.indexed(fromRange(1 to 500000)) + assert(F.forall(res) { case (a, i) => a == i + 1 }) + () + } + + test("Traverse[List].indexed consistent with zipWithIndex") { + forAll { (fa: List[String]) => + Traverse[List].indexed(fa) should === (fa.zipWithIndex) + } + } + + test("Traverse[List].indexed stack safety") { + checkIndexedStackSafety[List](_.toList) + } + + test("Traverse[Stream].indexed stack safety") { + checkIndexedStackSafety[Stream](_.toStream) + } +}