Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added zipWithLongIndex, mapWithLongIndex and traverseWithLongIndexM #4247

Merged
merged 4 commits into from
Jul 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions core/src/main/scala/cats/Traverse.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]] =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem like this should require Monad.

I think I see why this implementation does (I think due to using State).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnynek the scaladoc for the Int version explains:

  /**
   * Akin to [[traverse]], but also provides the value's index in
   * structure F when calling the function.
   *
   * This performs the traversal in a single pass but requires that
   * effect G is monadic. An applicative traversal can be performed in
   * two passes using [[zipWithIndex]] followed by [[traverse]].
   */

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)

Expand Down Expand Up @@ -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]
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/data/Chain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/data/NonEmptyChain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/data/NonEmptyList.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/data/NonEmptyVector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
10 changes: 10 additions & 0 deletions core/src/main/scala/cats/instances/StaticMethods.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding this! The next step is to use it to provide overrides of mapWithLongIndex in all the same places that the Int-version is used.
https://github.com/typelevel/cats/search?q=mapWithIndexFromStrictFunctor

Copy link
Contributor Author

@nikololiahim nikololiahim Jun 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be done in 6fe035f. I don't know if I should update the tests too, or these implementations are used automatically somehow.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not entirely sure of the question :) Actually, the behavior of the implementations should be completely identical, and the tests should be verifying that. Nothing "automatic" going on AFAIK :)

var idx: Long = 0L

ev.map(fa) { a =>
val b = f(a, idx)
idx += 1
b
}
}

}
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/instances/list.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/instances/queue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/instances/vector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 18 additions & 0 deletions laws/src/main/scala/cats/laws/TraverseLaws.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions tests/shared/src/test/scala/cats/tests/TraverseSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand All @@ -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)
Expand All @@ -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))
Expand Down