diff --git a/core/src/main/scala/cats/Traverse.scala b/core/src/main/scala/cats/Traverse.scala index f57300248e..8b753660e1 100644 --- a/core/src/main/scala/cats/Traverse.scala +++ b/core/src/main/scala/cats/Traverse.scala @@ -168,6 +168,24 @@ trait Traverse[F[_]] extends Functor[F] with Foldable[F] with UnorderedTraverse[ def zipWithIndex[A](fa: F[A]): F[(A, Int)] = mapWithIndex(fa)((a, i) => (a, i)) + /** + * Same as [[traverseWithIndexM]] but the index type is [[Long]] instead of [[Int]]. + */ + def traverseWithLongIndexM[G[_], A, B](fa: F[A])(f: (A, Long) => G[B])(implicit G: Monad[G]): G[F[B]] = + traverse(fa)(a => StateT((s: Long) => G.map(f(a, s))(b => (s + 1, b)))).runA(0L) + + /** + * Same as [[mapWithIndex]] but the index type is [[Long]] instead of [[Int]]. + */ + def mapWithLongIndex[A, B](fa: F[A])(f: (A, Long) => B): F[B] = + traverseWithLongIndexM[cats.Id, A, B](fa)((a, long) => f(a, long)) + + /** + * Same as [[zipWithIndex]] but the index type is [[Long]] instead of [[Int]]. + */ + def zipWithLongIndex[A](fa: F[A]): F[(A, Long)] = + mapWithLongIndex(fa)((a, long) => (a, long)) + override def unorderedTraverse[G[_]: CommutativeApplicative, A, B](sa: F[A])(f: (A) => G[B]): G[F[B]] = traverse(sa)(f) @@ -215,6 +233,12 @@ object Traverse { typeClassInstance.traverseWithIndexM[G, A, B](self)(f)(G) def zipWithIndex: F[(A, Int)] = typeClassInstance.zipWithIndex[A](self) + def zipWithLongIndex: F[(A, Long)] = + typeClassInstance.zipWithLongIndex[A](self) + def traverseWithLongIndexM[G[_], B](f: (A, Long) => G[B])(implicit G: Monad[G]): G[F[B]] = + typeClassInstance.traverseWithLongIndexM[G, A, B](self)(f) + def mapWithLongIndex[B](f: (A, Long) => B): F[B] = + typeClassInstance.mapWithLongIndex[A, B](self)(f) } trait AllOps[F[_], A] extends Ops[F, A] diff --git a/core/src/main/scala/cats/data/Chain.scala b/core/src/main/scala/cats/data/Chain.scala index 0cbf2839b9..112c890d7b 100644 --- a/core/src/main/scala/cats/data/Chain.scala +++ b/core/src/main/scala/cats/data/Chain.scala @@ -1256,6 +1256,9 @@ sealed abstract private[data] class ChainInstances extends ChainInstances1 { override def mapWithIndex[A, B](fa: Chain[A])(f: (A, Int) => B): Chain[B] = StaticMethods.mapWithIndexFromStrictFunctor(fa, f)(this) + override def mapWithLongIndex[A, B](fa: Chain[A])(f: (A, Long) => B): Chain[B] = + StaticMethods.mapWithLongIndexFromStrictFunctor(fa, f)(this) + override def zipWithIndex[A](fa: Chain[A]): Chain[(A, Int)] = fa.zipWithIndex diff --git a/core/src/main/scala/cats/data/NonEmptyChain.scala b/core/src/main/scala/cats/data/NonEmptyChain.scala index d2f1968fe1..7c8e70c90e 100644 --- a/core/src/main/scala/cats/data/NonEmptyChain.scala +++ b/core/src/main/scala/cats/data/NonEmptyChain.scala @@ -639,6 +639,9 @@ sealed abstract private[data] class NonEmptyChainInstances extends NonEmptyChain override def mapWithIndex[A, B](fa: NonEmptyChain[A])(f: (A, Int) => B): NonEmptyChain[B] = StaticMethods.mapWithIndexFromStrictFunctor(fa, f)(this) + override def mapWithLongIndex[A, B](fa: NonEmptyChain[A])(f: (A, Long) => B): NonEmptyChain[B] = + StaticMethods.mapWithLongIndexFromStrictFunctor(fa, f)(this) + override def zipWithIndex[A](fa: NonEmptyChain[A]): NonEmptyChain[(A, Int)] = fa.zipWithIndex diff --git a/core/src/main/scala/cats/data/NonEmptyList.scala b/core/src/main/scala/cats/data/NonEmptyList.scala index d72827b63f..aff19f3a8c 100644 --- a/core/src/main/scala/cats/data/NonEmptyList.scala +++ b/core/src/main/scala/cats/data/NonEmptyList.scala @@ -794,6 +794,9 @@ sealed abstract private[data] class NonEmptyListInstances extends NonEmptyListIn override def mapWithIndex[A, B](fa: NonEmptyList[A])(f: (A, Int) => B): NonEmptyList[B] = StaticMethods.mapWithIndexFromStrictFunctor(fa, f)(this) + override def mapWithLongIndex[A, B](fa: NonEmptyList[A])(f: (A, Long) => B): NonEmptyList[B] = + StaticMethods.mapWithLongIndexFromStrictFunctor(fa, f)(this) + override def zipWithIndex[A](fa: NonEmptyList[A]): NonEmptyList[(A, Int)] = fa.zipWithIndex diff --git a/core/src/main/scala/cats/data/NonEmptyVector.scala b/core/src/main/scala/cats/data/NonEmptyVector.scala index 22117f86da..18e7d55963 100644 --- a/core/src/main/scala/cats/data/NonEmptyVector.scala +++ b/core/src/main/scala/cats/data/NonEmptyVector.scala @@ -442,6 +442,9 @@ sealed abstract private[data] class NonEmptyVectorInstances { ): (S, NonEmptyVector[B]) = StaticMethods.mapAccumulateFromStrictFunctor(init, fa, f)(this) + override def mapWithLongIndex[A, B](fa: NonEmptyVector[A])(f: (A, Long) => B): NonEmptyVector[B] = + StaticMethods.mapWithLongIndexFromStrictFunctor(fa, f)(this) + override def mapWithIndex[A, B](fa: NonEmptyVector[A])(f: (A, Int) => B): NonEmptyVector[B] = StaticMethods.mapWithIndexFromStrictFunctor(fa, f)(this) diff --git a/core/src/main/scala/cats/instances/StaticMethods.scala b/core/src/main/scala/cats/instances/StaticMethods.scala index 24cbfad4b6..8df598bddd 100644 --- a/core/src/main/scala/cats/instances/StaticMethods.scala +++ b/core/src/main/scala/cats/instances/StaticMethods.scala @@ -60,4 +60,14 @@ private[cats] object StaticMethods { } } + def mapWithLongIndexFromStrictFunctor[F[_], A, B](fa: F[A], f: (A, Long) => B)(implicit ev: Functor[F]): F[B] = { + var idx: Long = 0L + + ev.map(fa) { a => + val b = f(a, idx) + idx += 1 + b + } + } + } diff --git a/core/src/main/scala/cats/instances/list.scala b/core/src/main/scala/cats/instances/list.scala index 979438171a..e65cb5b193 100644 --- a/core/src/main/scala/cats/instances/list.scala +++ b/core/src/main/scala/cats/instances/list.scala @@ -188,6 +188,9 @@ trait ListInstances extends cats.kernel.instances.ListInstances { override def mapAccumulate[S, A, B](init: S, fa: List[A])(f: (S, A) => (S, B)): (S, List[B]) = StaticMethods.mapAccumulateFromStrictFunctor(init, fa, f)(this) + override def mapWithLongIndex[A, B](fa: List[A])(f: (A, Long) => B): List[B] = + StaticMethods.mapWithLongIndexFromStrictFunctor(fa, f)(this) + override def mapWithIndex[A, B](fa: List[A])(f: (A, Int) => B): List[B] = StaticMethods.mapWithIndexFromStrictFunctor(fa, f)(this) diff --git a/core/src/main/scala/cats/instances/queue.scala b/core/src/main/scala/cats/instances/queue.scala index ed16624492..a54c920992 100644 --- a/core/src/main/scala/cats/instances/queue.scala +++ b/core/src/main/scala/cats/instances/queue.scala @@ -132,6 +132,9 @@ trait QueueInstances extends cats.kernel.instances.QueueInstances { override def mapAccumulate[S, A, B](init: S, fa: Queue[A])(f: (S, A) => (S, B)): (S, Queue[B]) = StaticMethods.mapAccumulateFromStrictFunctor(init, fa, f)(this) + override def mapWithLongIndex[A, B](fa: Queue[A])(f: (A, Long) => B): Queue[B] = + StaticMethods.mapWithLongIndexFromStrictFunctor(fa, f)(this) + override def mapWithIndex[A, B](fa: Queue[A])(f: (A, Int) => B): Queue[B] = StaticMethods.mapWithIndexFromStrictFunctor(fa, f)(this) diff --git a/core/src/main/scala/cats/instances/vector.scala b/core/src/main/scala/cats/instances/vector.scala index 5632b6bad3..31a003c6c0 100644 --- a/core/src/main/scala/cats/instances/vector.scala +++ b/core/src/main/scala/cats/instances/vector.scala @@ -174,6 +174,9 @@ trait VectorInstances extends cats.kernel.instances.VectorInstances { override def mapWithIndex[A, B](fa: Vector[A])(f: (A, Int) => B): Vector[B] = StaticMethods.mapWithIndexFromStrictFunctor(fa, f)(this) + override def mapWithLongIndex[A, B](fa: Vector[A])(f: (A, Long) => B): Vector[B] = + StaticMethods.mapWithLongIndexFromStrictFunctor(fa, f)(this) + override def zipWithIndex[A](fa: Vector[A]): Vector[(A, Int)] = fa.zipWithIndex diff --git a/laws/src/main/scala/cats/laws/TraverseLaws.scala b/laws/src/main/scala/cats/laws/TraverseLaws.scala index cfcb419ff2..ce1c23cda6 100644 --- a/laws/src/main/scala/cats/laws/TraverseLaws.scala +++ b/laws/src/main/scala/cats/laws/TraverseLaws.scala @@ -129,6 +129,24 @@ trait TraverseLaws[F[_]] extends FunctorLaws[F] with FoldableLaws[F] with Unorde val rhs = F.map(F.mapWithIndex(fa)((a, i) => (a, i)))(f) lhs <-> rhs } + + def mapWithLongIndexRef[A, B](fa: F[A], f: (A, Long) => B): IsEq[F[B]] = { + val lhs = F.mapWithLongIndex(fa)(f) + val rhs = F.traverse(fa)(a => State((s: Long) => (s + 1, f(a, s)))).runA(0L).value + lhs <-> rhs + } + + def traverseWithLongIndexMRef[G[_], A, B](fa: F[A], f: (A, Long) => G[B])(implicit G: Monad[G]): IsEq[G[F[B]]] = { + val lhs = F.traverseWithLongIndexM(fa)(f) + val rhs = F.traverse(fa)(a => StateT((s: Long) => G.map(f(a, s))(b => (s + 1, b)))).runA(0L) + lhs <-> rhs + } + + def zipWithLongIndexRef[A, B](fa: F[A], f: ((A, Long)) => B): IsEq[F[B]] = { + val lhs = F.map(F.zipWithLongIndex(fa))(f) + val rhs = F.map(F.mapWithLongIndex(fa)((a, i) => (a, i)))(f) + lhs <-> rhs + } } object TraverseLaws { diff --git a/tests/shared/src/test/scala/cats/tests/TraverseSuite.scala b/tests/shared/src/test/scala/cats/tests/TraverseSuite.scala index 4b893758e1..07afa07a3d 100644 --- a/tests/shared/src/test/scala/cats/tests/TraverseSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/TraverseSuite.scala @@ -36,6 +36,12 @@ abstract class TraverseSuite[F[_]: Traverse](name: String)(implicit ArbFInt: Arb } } + test(s"Traverse[$name].zipWithLongIndex") { + forAll { (fa: F[Int]) => + assert(fa.zipWithLongIndex.toList === (fa.toList.zipWithLongIndex)) + } + } + test(s"Traverse[$name].mapAccumulate") { forAll { (init: Int, fa: F[Int], fn: ((Int, Int)) => (Int, Int)) => val lhs = fa.mapAccumulate(init)((s, a) => fn((s, a))) @@ -55,6 +61,12 @@ abstract class TraverseSuite[F[_]: Traverse](name: String)(implicit ArbFInt: Arb } } + test(s"Traverse[$name].mapWithLongIndex") { + forAll { (fa: F[Int], fn: ((Int, Long)) => Int) => + assert(fa.mapWithLongIndex((a, i) => fn((a, i))).toList === (fa.toList.zipWithLongIndex.map(fn))) + } + } + test(s"Traverse[$name].traverseWithIndexM") { forAll { (fa: F[Int], fn: ((Int, Int)) => (Int, Int)) => val left = fa.traverseWithIndexM((a, i) => fn((a, i))).fmap(_.toList) @@ -63,6 +75,14 @@ abstract class TraverseSuite[F[_]: Traverse](name: String)(implicit ArbFInt: Arb } } + test(s"Traverse[$name].traverseWithLongIndexM") { + forAll { (fa: F[Int], fn: ((Int, Long)) => (Int, Long)) => + val left = fa.traverseWithLongIndexM((a, i) => fn((a, i))).fmap(_.toList) + val (xs, values) = fa.toList.zipWithLongIndex.map(fn).unzip + assert(left === ((xs.combineAll, values))) + } + } + test(s"Traverse[$name].traverse matches traverse_ with Option") { forAll { (fa: F[Int], fn: Int => Option[Int]) => assert(Applicative[Option].void(fa.traverse(fn)) == fa.traverse_(fn))