Skip to content
48 changes: 48 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,46 @@ 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))

/**
* If `fa` contains the element at index `idx`,
* return the copy of `fa` where the element at `idx` is replaced with `b`.
* If there is no element with such an index, return `None`.
*
* The behavior is consistent with the Scala collection library's
* `updated` for collections such as `List`.
*/
def updated_[A, B >: A](fa: F[A], idx: Long, b: B): Option[F[B]] = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry, I didn't realize we are missing a law for this! It can be like the other laws, just verifying against the reference implementation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@armanbilge I am a little lost here, what should the reference implementation be in this case?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ended up adding one of the previous implementations in bdf2d41. Is it okay?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, the reference implementation should be the same as the default implementation :) basically, it's a way to make sure that if someone overrides it (like we do in Vector) it matches what the default implementation would do.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Btw, sorry I wrote that so confusingly, I should have just said "default implementation". I was thinking about how these laws end in Ref which I assume stands for "reference".

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ah, so that's why they are all the same as default. I thought it was weird that they are all just the same as the trait impl. Like, what are we even testing here? The fact that some instance may override certain default impl totally slipped from my head. Now it all makes sense, thank you!

if (idx < 0L)
None
else
mapAccumulate(0L, fa)((i, a) =>
if (i == idx)
(i + 1, b)
else
(i + 1, a)
) match {
case (i, fb) if i > idx => Some(fb)
case _ => None
}
}
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 +255,14 @@ 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)
def updated_[B >: A](idx: Long, b: B): Option[F[B]] =
typeClassInstance.updated_(self, idx, b)
}
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
6 changes: 6 additions & 0 deletions core/src/main/scala/cats/data/NonEmptyVector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -442,12 +442,18 @@ 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)

override def zipWithIndex[A](fa: NonEmptyVector[A]): NonEmptyVector[(A, Int)] =
fa.zipWithIndex

override def updated_[A, B >: A](fa: NonEmptyVector[A], idx: Long, b: B): Option[NonEmptyVector[B]] =
Traverse[Vector].updated_(fa.toVector, idx, b).map(NonEmptyVector.fromVectorUnsafe)

override def foldLeft[A, B](fa: NonEmptyVector[A], b: B)(f: (B, A) => B): B =
fa.foldLeft(b)(f)

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] = {
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
10 changes: 10 additions & 0 deletions core/src/main/scala/cats/instances/vector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ trait VectorInstances extends cats.kernel.instances.VectorInstances {
final override def traverse[G[_], A, B](fa: Vector[A])(f: A => G[B])(implicit G: Applicative[G]): G[Vector[B]] =
G.map(Chain.traverseViaChain(fa)(f))(_.toVector)

final override def updated_[A, B >: A](fa: Vector[A], idx: Long, b: B): Option[Vector[B]] =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can add a similar override to NonEmptyVector.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added one in f9a00e6.

if (idx >= 0L && idx < fa.size.toLong) {
Some(fa.updated(idx.toInt, b))
} else {
None
}

/**
* This avoids making a very deep stack by building a tree instead
*/
Expand Down Expand Up @@ -174,6 +181,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
37 changes: 37 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,43 @@ 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]] = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@nikololiahim I just realized, we added the laws but not the tests 😅 I completely forgot about those, so sorry!

"traverse identity" -> forAll(laws.traverseIdentity[A, C] _),
"traverse sequential composition" -> forAll(laws.traverseSequentialComposition[A, B, C, X, Y] _),
"traverse parallel composition" -> forAll(laws.traverseParallelComposition[A, B, X, Y] _),
"traverse traverseTap" -> forAll(laws.traverseTap[B, M, X] _),
"traverse derive foldMap" -> forAll(laws.foldMapDerived[A, M] _),
"traverse order consistency" -> forAll(laws.traverseOrderConsistent[A] _),
"traverse ref mapAccumulate" -> forAll(laws.mapAccumulateRef[M, A, C] _),
"traverse ref mapWithIndex" -> forAll(laws.mapWithIndexRef[A, C] _),
"traverse ref traverseWithIndexM" -> forAll(laws.traverseWithIndexMRef[Option, A, C] _),
"traverse ref zipWithIndex" -> forAll(laws.zipWithIndexRef[A, C] _)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Should I reopen and add them here? Or open a separate PR?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Whatever works, although I'm not sure if it's possible to re-open a merged PR :) thanks!!

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
}

def updatedRef[A, B >: A](fa: F[A], idx: Long, b: B): IsEq[Option[F[B]]] = {
val lhs = F.updated_(fa, idx, b)
val rhs =
if (idx < 0L)
None
else
F.mapAccumulate(0L, fa)((i, a) =>
if (i == idx)
(i + 1, b)
else
(i + 1, a)
) match {
case (i, fb) if i > idx => Some(fb)
case _ => None
}

lhs <-> rhs
}
}

object TraverseLaws {
Expand Down
28 changes: 28 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,12 +75,28 @@ 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))
}
}

test(s"Traverse[$name].updated_") {
forAll { (fa: F[Int], i: Int, b: Int) =>
val updatedThenToList: Option[List[Int]] = fa.updated_(i.toLong, b).fmap(_.toList)
val toListThenUpdated: Option[List[Int]] = scala.util.Try(fa.toList.updated(i, b)).toOption
assertEquals(updatedThenToList, toListThenUpdated)
}
}

}

object TraverseSuite {
Expand Down