diff --git a/bench/src/main/scala/cats/bench/TraverseBench.scala b/bench/src/main/scala/cats/bench/TraverseBench.scala index dd2cc1e048..188b1f6a43 100644 --- a/bench/src/main/scala/cats/bench/TraverseBench.scala +++ b/bench/src/main/scala/cats/bench/TraverseBench.scala @@ -24,15 +24,19 @@ package cats.bench import cats.{Eval, Traverse, TraverseFilter} import org.openjdk.jmh.annotations.{Benchmark, Param, Scope, Setup, State} import org.openjdk.jmh.infra.Blackhole +import cats.data.Chain @State(Scope.Benchmark) class TraverseBench { val listT: Traverse[List] = Traverse[List] val listTFilter: TraverseFilter[List] = TraverseFilter[List] + val chainTFilter: TraverseFilter[Chain] = TraverseFilter[Chain] val vectorT: Traverse[Vector] = Traverse[Vector] val vectorTFilter: TraverseFilter[Vector] = TraverseFilter[Vector] + val chainT: Traverse[Chain] = Traverse[Chain] + // the unit of CPU work per iteration private[this] val Work: Long = 10 @@ -43,11 +47,13 @@ class TraverseBench { var list: List[Int] = _ var vector: Vector[Int] = _ + var chain: Chain[Int] = _ @Setup def setup(): Unit = { list = 0.until(length).toList vector = 0.until(length).toVector + chain = Chain.fromSeq(0.until(length)) } @Benchmark @@ -83,6 +89,18 @@ class TraverseBench { } } + @Benchmark + def traverse_List(bh: Blackhole) = { + val result = listT.traverse_(list) { i => + Eval.later { + Blackhole.consumeCPU(Work) + i * 2 + } + } + + bh.consume(result.value) + } + @Benchmark def traverseFilterList(bh: Blackhole) = { val result = listTFilter.traverseFilter(list) { i => @@ -137,6 +155,18 @@ class TraverseBench { bh.consume(result.value) } + @Benchmark + def traverse_Vector(bh: Blackhole) = { + val result = vectorT.traverse_(vector) { i => + Eval.later { + Blackhole.consumeCPU(Work) + i * 2 + } + } + + bh.consume(result.value) + } + @Benchmark def traverseVectorError(bh: Blackhole) = { val result = vectorT.traverse(vector) { i => @@ -199,4 +229,61 @@ class TraverseBench { bh.consume(results) } + + @Benchmark + def traverseChain(bh: Blackhole) = { + val result = chainT.traverse(chain) { i => + Eval.later { + Blackhole.consumeCPU(Work) + i * 2 + } + } + + bh.consume(result.value) + } + + @Benchmark + def traverse_Chain(bh: Blackhole) = { + val result = chainT.traverse_(chain) { i => + Eval.later { + Blackhole.consumeCPU(Work) + i * 2 + } + } + + bh.consume(result.value) + } + + @Benchmark + def traverseChainError(bh: Blackhole) = { + val result = chainT.traverse(chain) { i => + Eval.later { + Blackhole.consumeCPU(Work) + + if (i == length * 0.3) { + throw Failure + } + + i * 2 + } + } + + try { + bh.consume(result.value) + } catch { + case Failure => () + } + } + + @Benchmark + def traverseFilterChain(bh: Blackhole) = { + val result = chainTFilter.traverseFilter(chain) { i => + Eval.later { + Blackhole.consumeCPU(Work) + if (i % 2 == 0) Some(i * 2) else None + } + } + + bh.consume(result.value) + } } diff --git a/core/src/main/scala-2.13+/cats/instances/arraySeq.scala b/core/src/main/scala-2.13+/cats/instances/arraySeq.scala index 355664b0be..f41a781ad6 100644 --- a/core/src/main/scala-2.13+/cats/instances/arraySeq.scala +++ b/core/src/main/scala-2.13+/cats/instances/arraySeq.scala @@ -102,7 +102,24 @@ private[cats] object ArraySeqInstances { B.combineAll(fa.iterator.map(f)) def traverse[G[_], A, B](fa: ArraySeq[A])(f: A => G[B])(implicit G: Applicative[G]): G[ArraySeq[B]] = - G.map(Chain.traverseViaChain(fa)(f))(_.iterator.to(ArraySeq.untagged)) + G match { + case x: StackSafeMonad[G] => + x.map(Traverse.traverseDirectly(fa.iterator)(f)(x))(_.iterator.to(ArraySeq.untagged)) + case _ => + G.map(Chain.traverseViaChain(fa)(f))(_.iterator.to(ArraySeq.untagged)) + + } + + override def traverse_[G[_], A, B](fa: ArraySeq[A])(f: A => G[B])(implicit G: Applicative[G]): G[Unit] = + G match { + case x: StackSafeMonad[G] => Traverse.traverse_Directly(fa)(f)(x) + case _ => + foldRight(fa, Eval.now(G.unit)) { (a, acc) => + G.map2Eval(f(a), acc) { (_, _) => + () + } + }.value + } override def mapAccumulate[S, A, B](init: S, fa: ArraySeq[A])(f: (S, A) => (S, B)): (S, ArraySeq[B]) = StaticMethods.mapAccumulateFromStrictFunctor(init, fa, f)(this) @@ -214,9 +231,17 @@ private[cats] object ArraySeqInstances { def traverseFilter[G[_], A, B]( fa: ArraySeq[A] )(f: (A) => G[Option[B]])(implicit G: Applicative[G]): G[ArraySeq[B]] = - fa.foldRight(Eval.now(G.pure(ArraySeq.untagged.empty[B]))) { case (x, xse) => - G.map2Eval(f(x), xse)((i, o) => i.fold(o)(_ +: o)) - }.value + G match { + case x: StackSafeMonad[G] => + x.map(TraverseFilter.traverseFilterDirectly(fa.iterator)(f)(x))( + _.iterator.to(ArraySeq.untagged) + ) + case _ => + fa.foldRight(Eval.now(G.pure(ArraySeq.untagged.empty[B]))) { case (x, xse) => + G.map2Eval(f(x), xse)((i, o) => i.fold(o)(_ +: o)) + }.value + + } override def filterA[G[_], A](fa: ArraySeq[A])(f: (A) => G[Boolean])(implicit G: Applicative[G]): G[ArraySeq[A]] = fa.foldRight(Eval.now(G.pure(ArraySeq.untagged.empty[A]))) { case (x, xse) => diff --git a/core/src/main/scala/cats/Traverse.scala b/core/src/main/scala/cats/Traverse.scala index f0f0ac0a17..cc2993ee69 100644 --- a/core/src/main/scala/cats/Traverse.scala +++ b/core/src/main/scala/cats/Traverse.scala @@ -21,8 +21,10 @@ package cats +import cats.data.Chain import cats.data.State import cats.data.StateT +import cats.kernel.compat.scalaVersionSpecific._ /** * Traverse, also known as Traversable. @@ -284,4 +286,26 @@ object Traverse { @deprecated("Use cats.syntax object imports", "2.2.0") object nonInheritedOps extends ToTraverseOps + private[cats] def traverseDirectly[G[_], A, B]( + fa: IterableOnce[A] + )(f: A => G[B])(implicit G: StackSafeMonad[G]): G[Chain[B]] = { + fa.iterator.foldLeft(G.pure(Chain.empty[B])) { case (accG, a) => + G.map2(accG, f(a)) { case (acc, x) => + acc :+ x + } + } + } + + private[cats] def traverse_Directly[G[_], A, B]( + fa: IterableOnce[A] + )(f: A => G[B])(implicit G: StackSafeMonad[G]): G[Unit] = { + val iter = fa.iterator + if (iter.hasNext) { + val first = iter.next() + G.void(iter.foldLeft(f(first)) { case (g, a) => + G.productR(g)(f(a)) + }) + } else G.unit + } + } diff --git a/core/src/main/scala/cats/TraverseFilter.scala b/core/src/main/scala/cats/TraverseFilter.scala index 2f44b0f7c3..0907ef3be7 100644 --- a/core/src/main/scala/cats/TraverseFilter.scala +++ b/core/src/main/scala/cats/TraverseFilter.scala @@ -21,7 +21,8 @@ package cats -import cats.data.State +import cats.data.{Chain, State} +import cats.kernel.compat.scalaVersionSpecific._ import scala.collection.immutable.{IntMap, TreeSet} @@ -203,4 +204,15 @@ object TraverseFilter { @deprecated("Use cats.syntax object imports", "2.2.0") object nonInheritedOps extends ToTraverseFilterOps + private[cats] def traverseFilterDirectly[G[_], A, B]( + fa: IterableOnce[A] + )(f: A => G[Option[B]])(implicit G: StackSafeMonad[G]): G[Chain[B]] = { + fa.iterator.foldLeft(G.pure(Chain.empty[B])) { case (bldrG, a) => + G.map2(bldrG, f(a)) { + case (acc, Some(b)) => acc :+ b + case (acc, None) => acc + } + } + } + } diff --git a/core/src/main/scala/cats/data/Chain.scala b/core/src/main/scala/cats/data/Chain.scala index 69c0d2da5e..2248a562ea 100644 --- a/core/src/main/scala/cats/data/Chain.scala +++ b/core/src/main/scala/cats/data/Chain.scala @@ -1243,11 +1243,27 @@ sealed abstract private[data] class ChainInstances extends ChainInstances1 { def traverse[G[_], A, B](fa: Chain[A])(f: A => G[B])(implicit G: Applicative[G]): G[Chain[B]] = if (fa.isEmpty) G.pure(Chain.nil) else - traverseViaChain { - val as = collection.mutable.ArrayBuffer[A]() - as ++= fa.iterator - KernelStaticMethods.wrapMutableIndexedSeq(as) - }(f) + G match { + case x: StackSafeMonad[G] => + Traverse.traverseDirectly(fa.iterator)(f)(x) + case _ => + traverseViaChain { + val as = collection.mutable.ArrayBuffer[A]() + as ++= fa.iterator + KernelStaticMethods.wrapMutableIndexedSeq(as) + }(f) + } + + override def traverse_[G[_], A, B](fa: Chain[A])(f: A => G[B])(implicit G: Applicative[G]): G[Unit] = + G match { + case x: StackSafeMonad[G] => Traverse.traverse_Directly(fa.iterator)(f)(x) + case _ => + foldRight(fa, Eval.now(G.unit)) { (a, acc) => + G.map2Eval(f(a), acc) { (_, _) => + () + } + }.value + } override def mapAccumulate[S, A, B](init: S, fa: Chain[A])(f: (S, A) => (S, B)): (S, Chain[B]) = StaticMethods.mapAccumulateFromStrictFunctor(init, fa, f)(this) @@ -1341,7 +1357,7 @@ sealed abstract private[data] class ChainInstances extends ChainInstances1 { } implicit val catsDataTraverseFilterForChain: TraverseFilter[Chain] = new TraverseFilter[Chain] { - def traverse: Traverse[Chain] = Chain.catsDataInstancesForChain + def traverse: Traverse[Chain] with Alternative[Chain] = Chain.catsDataInstancesForChain override def filter[A](fa: Chain[A])(f: A => Boolean): Chain[A] = fa.filter(f) @@ -1356,11 +1372,16 @@ sealed abstract private[data] class ChainInstances extends ChainInstances1 { def traverseFilter[G[_], A, B](fa: Chain[A])(f: A => G[Option[B]])(implicit G: Applicative[G]): G[Chain[B]] = if (fa.isEmpty) G.pure(Chain.nil) else - traverseFilterViaChain { - val as = collection.mutable.ArrayBuffer[A]() - as ++= fa.iterator - KernelStaticMethods.wrapMutableIndexedSeq(as) - }(f) + G match { + case x: StackSafeMonad[G] => + TraverseFilter.traverseFilterDirectly(fa.iterator)(f)(x) + case _ => + traverseFilterViaChain { + val as = collection.mutable.ArrayBuffer[A]() + as ++= fa.iterator + KernelStaticMethods.wrapMutableIndexedSeq(as) + }(f) + } override def filterA[G[_], A](fa: Chain[A])(f: A => G[Boolean])(implicit G: Applicative[G]): G[Chain[A]] = traverse diff --git a/core/src/main/scala/cats/instances/list.scala b/core/src/main/scala/cats/instances/list.scala index f4d01cda86..a1306f159b 100644 --- a/core/src/main/scala/cats/instances/list.scala +++ b/core/src/main/scala/cats/instances/list.scala @@ -23,6 +23,7 @@ package cats package instances import cats.data.{Chain, Ior, ZipList} +import cats.StackSafeMonad import cats.instances.StaticMethods.appendAll import cats.kernel.compat.scalaVersionSpecific._ import cats.kernel.instances.StaticMethods.wrapMutableIndexedSeq @@ -120,50 +121,58 @@ trait ListInstances extends cats.kernel.instances.ListInstances { def traverse[G[_], A, B](fa: List[A])(f: A => G[B])(implicit G: Applicative[G]): G[List[B]] = if (fa.isEmpty) G.pure(Nil) else - G.map(Chain.traverseViaChain { - val as = collection.mutable.ArrayBuffer[A]() - as ++= fa - wrapMutableIndexedSeq(as) - }(f))(_.toList) + G match { + case x: StackSafeMonad[G] => x.map(Traverse.traverseDirectly[G, A, B](fa)(f)(x))(_.toList) + case _ => + G.map(Chain.traverseViaChain { + val as = collection.mutable.ArrayBuffer[A]() + as ++= fa + wrapMutableIndexedSeq(as) + }(f))(_.toList) + } /** * This avoids making a very deep stack by building a tree instead */ override def traverse_[G[_], A, B](fa: List[A])(f: A => G[B])(implicit G: Applicative[G]): G[Unit] = { - // the cost of this is O(size log size) - // c(n) = n + 2 * c(n/2) = n + 2(n/2 log (n/2)) = n + n (logn - 1) = n log n - // invariant: size >= 1 - def runHalf(size: Int, fa: List[A]): Eval[G[Unit]] = - if (size > 1) { - val leftSize = size / 2 - val rightSize = size - leftSize - val (leftL, rightL) = fa.splitAt(leftSize) - runHalf(leftSize, leftL) - .flatMap { left => - val right = runHalf(rightSize, rightL) - G.map2Eval(left, right) { (_, _) => () } + G match { + case x: StackSafeMonad[G] => Traverse.traverse_Directly(fa)(f)(x) + case _ => + // the cost of this is O(size log size) + // c(n) = n + 2 * c(n/2) = n + 2(n/2 log (n/2)) = n + n (logn - 1) = n log n + // invariant: size >= 1 + def runHalf(size: Int, fa: List[A]): Eval[G[Unit]] = + if (size > 1) { + val leftSize = size / 2 + val rightSize = size - leftSize + val (leftL, rightL) = fa.splitAt(leftSize) + runHalf(leftSize, leftL) + .flatMap { left => + val right = runHalf(rightSize, rightL) + G.map2Eval(left, right) { (_, _) => () } + } + } else { + // avoid pattern matching when we know that there is only one element + val a = fa.head + // we evaluate this at most one time, + // always is a bit cheaper in such cases + // + // Here is the point of the laziness using Eval: + // we avoid calling f(a) or G.void in the + // event that the computation has already + // failed. We do not use laziness to avoid + // traversing fa, which we will do fully + // in all cases. + Eval.always { + val gb = f(a) + G.void(gb) + } } - } else { - // avoid pattern matching when we know that there is only one element - val a = fa.head - // we evaluate this at most one time, - // always is a bit cheaper in such cases - // - // Here is the point of the laziness using Eval: - // we avoid calling f(a) or G.void in the - // event that the computation has already - // failed. We do not use laziness to avoid - // traversing fa, which we will do fully - // in all cases. - Eval.always { - val gb = f(a) - G.void(gb) - } - } - val len = fa.length - if (len == 0) G.unit - else runHalf(len, fa).value + val len = fa.length + if (len == 0) G.unit + else runHalf(len, fa).value + } } def functor: Functor[List] = this @@ -310,11 +319,15 @@ private[instances] trait ListInstancesBinCompat0 { def traverseFilter[G[_], A, B](fa: List[A])(f: (A) => G[Option[B]])(implicit G: Applicative[G]): G[List[B]] = if (fa.isEmpty) G.pure(Nil) else - G.map(Chain.traverseFilterViaChain { - val as = collection.mutable.ArrayBuffer[A]() - as ++= fa - wrapMutableIndexedSeq(as) - }(f))(_.toList) + G match { + case x: StackSafeMonad[G] => x.map(TraverseFilter.traverseFilterDirectly(fa)(f)(x))(_.toList) + case _ => + G.map(Chain.traverseFilterViaChain { + val as = collection.mutable.ArrayBuffer[A]() + as ++= fa + wrapMutableIndexedSeq(as) + }(f))(_.toList) + } override def filterA[G[_], A](fa: List[A])(f: (A) => G[Boolean])(implicit G: Applicative[G]): G[List[A]] = traverse diff --git a/core/src/main/scala/cats/instances/map.scala b/core/src/main/scala/cats/instances/map.scala index 82460f4879..c469f88b5d 100644 --- a/core/src/main/scala/cats/instances/map.scala +++ b/core/src/main/scala/cats/instances/map.scala @@ -44,9 +44,16 @@ trait MapInstances extends cats.kernel.instances.MapInstances { )(f: A => G[B])(implicit G: CommutativeApplicative[G]): G[Map[K, B]] = { if (fa.isEmpty) G.pure(Map.empty[K, B]) else - G.map(Chain.traverseViaChain(fa.toIndexedSeq) { case (k, a) => - G.map(f(a))((k, _)) - })(_.iterator.toMap) + G match { + case x: StackSafeMonad[G] => + fa.iterator.foldLeft(G.pure(Map.empty[K, B])) { case (accG, (k, a)) => + x.map2(accG, f(a)) { case (acc, b) => acc + (k -> b) } + } + case _ => + G.map(Chain.traverseViaChain(fa.toIndexedSeq) { case (k, a) => + G.map(f(a))((k, _)) + })(_.iterator.toMap) + } } override def map[A, B](fa: Map[K, A])(f: A => B): Map[K, B] = diff --git a/core/src/main/scala/cats/instances/queue.scala b/core/src/main/scala/cats/instances/queue.scala index 1bfbc52af6..bb10850423 100644 --- a/core/src/main/scala/cats/instances/queue.scala +++ b/core/src/main/scala/cats/instances/queue.scala @@ -121,14 +121,30 @@ trait QueueInstances extends cats.kernel.instances.QueueInstances { def traverse[G[_], A, B](fa: Queue[A])(f: A => G[B])(implicit G: Applicative[G]): G[Queue[B]] = if (fa.isEmpty) G.pure(Queue.empty[B]) else - G.map(Chain.traverseViaChain { - val as = collection.mutable.ArrayBuffer[A]() - as ++= fa - wrapMutableIndexedSeq(as) - }(f)) { chain => - chain.foldLeft(Queue.empty[B])(_ :+ _) + G match { + case x: StackSafeMonad[G] => + G.map(Traverse.traverseDirectly(fa)(f)(x))(c => fromIterableOnce(c.iterator)) + case _ => + G.map(Chain.traverseViaChain { + val as = collection.mutable.ArrayBuffer[A]() + as ++= fa + wrapMutableIndexedSeq(as) + }(f)) { chain => + chain.foldLeft(Queue.empty[B])(_ :+ _) + } } + override def traverse_[G[_], A, B](fa: Queue[A])(f: A => G[B])(implicit G: Applicative[G]): G[Unit] = + G match { + case x: StackSafeMonad[G] => Traverse.traverse_Directly(fa)(f)(x) + case _ => + foldRight(fa, Eval.now(G.unit)) { (a, acc) => + G.map2Eval(f(a), acc) { (_, _) => + () + } + }.value + } + 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) @@ -207,7 +223,7 @@ trait QueueInstances extends cats.kernel.instances.QueueInstances { @suppressUnusedImportWarningForScalaVersionSpecific private object QueueInstances { private val catsStdTraverseFilterForQueue: TraverseFilter[Queue] = new TraverseFilter[Queue] { - val traverse: Traverse[Queue] = cats.instances.queue.catsStdInstancesForQueue + val traverse: Traverse[Queue] with Alternative[Queue] = cats.instances.queue.catsStdInstancesForQueue override def mapFilter[A, B](fa: Queue[A])(f: (A) => Option[B]): Queue[B] = fa.collect(Function.unlift(f)) @@ -223,12 +239,17 @@ private object QueueInstances { def traverseFilter[G[_], A, B](fa: Queue[A])(f: (A) => G[Option[B]])(implicit G: Applicative[G]): G[Queue[B]] = if (fa.isEmpty) G.pure(Queue.empty[B]) else - G.map(Chain.traverseFilterViaChain { - val as = collection.mutable.ArrayBuffer[A]() - as ++= fa - wrapMutableIndexedSeq(as) - }(f)) { chain => - chain.foldLeft(Queue.empty[B])(_ :+ _) + G match { + case x: StackSafeMonad[G] => + x.map(TraverseFilter.traverseFilterDirectly(fa)(f)(x))(c => traverse.fromIterableOnce(c.iterator)) + case _ => + G.map(Chain.traverseFilterViaChain { + val as = collection.mutable.ArrayBuffer[A]() + as ++= fa + wrapMutableIndexedSeq(as) + }(f)) { chain => + chain.foldLeft(Queue.empty[B])(_ :+ _) + } } override def filterA[G[_], A](fa: Queue[A])(f: (A) => G[Boolean])(implicit G: Applicative[G]): G[Queue[A]] = diff --git a/core/src/main/scala/cats/instances/seq.scala b/core/src/main/scala/cats/instances/seq.scala index cdbf0adf27..32272eb1f9 100644 --- a/core/src/main/scala/cats/instances/seq.scala +++ b/core/src/main/scala/cats/instances/seq.scala @@ -127,7 +127,23 @@ trait SeqInstances extends cats.kernel.instances.SeqInstances { } final override def traverse[G[_], A, B](fa: Seq[A])(f: A => G[B])(implicit G: Applicative[G]): G[Seq[B]] = - G.map(Chain.traverseViaChain(fa.toIndexedSeq)(f))(_.toVector) + G match { + case x: StackSafeMonad[G] => + x.map(Traverse.traverseDirectly(fa)(f)(x))(_.toList) + case _ => + G.map(Chain.traverseViaChain(fa.toIndexedSeq)(f))(_.toList) + } + + override def traverse_[G[_], A, B](fa: Seq[A])(f: A => G[B])(implicit G: Applicative[G]): G[Unit] = + G match { + case x: StackSafeMonad[G] => Traverse.traverse_Directly(fa)(f)(x) + case _ => + foldRight(fa, Eval.now(G.unit)) { (a, acc) => + G.map2Eval(f(a), acc) { (_, _) => + () + } + }.value + } override def mapWithIndex[A, B](fa: Seq[A])(f: (A, Int) => B): Seq[B] = fa.zipWithIndex.map(ai => f(ai._1, ai._2)) @@ -193,7 +209,11 @@ trait SeqInstances extends cats.kernel.instances.SeqInstances { override def flattenOption[A](fa: Seq[Option[A]]): Seq[A] = fa.flatten def traverseFilter[G[_], A, B](fa: Seq[A])(f: (A) => G[Option[B]])(implicit G: Applicative[G]): G[Seq[B]] = - G.map(Chain.traverseFilterViaChain(fa.toIndexedSeq)(f))(_.toVector) + G match { + case x: StackSafeMonad[G] => x.map(TraverseFilter.traverseFilterDirectly(fa)(f)(x))(_.toVector) + case _ => + G.map(Chain.traverseFilterViaChain(fa.toIndexedSeq)(f))(_.toVector) + } override def filterA[G[_], A](fa: Seq[A])(f: (A) => G[Boolean])(implicit G: Applicative[G]): G[Seq[A]] = traverse diff --git a/core/src/main/scala/cats/instances/vector.scala b/core/src/main/scala/cats/instances/vector.scala index 88db57d2b4..06f93b4fef 100644 --- a/core/src/main/scala/cats/instances/vector.scala +++ b/core/src/main/scala/cats/instances/vector.scala @@ -125,7 +125,10 @@ 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) + G match { + case x: StackSafeMonad[G] => x.map(Traverse.traverseDirectly(fa)(f)(x))(_.toVector) + case _ => G.map(Chain.traverseViaChain(fa)(f))(_.toVector) + } final override def updated_[A, B >: A](fa: Vector[A], idx: Long, b: B): Option[Vector[B]] = if (idx >= 0L && idx < fa.size.toLong) { @@ -138,38 +141,42 @@ trait VectorInstances extends cats.kernel.instances.VectorInstances { * This avoids making a very deep stack by building a tree instead */ override def traverse_[G[_], A, B](fa: Vector[A])(f: A => G[B])(implicit G: Applicative[G]): G[Unit] = { - // the cost of this is O(size) - // c(n) = 1 + 2 * c(n/2) - // invariant: size >= 1 - def runHalf(size: Int, idx: Int): Eval[G[Unit]] = - if (size > 1) { - val leftSize = size / 2 - val rightSize = size - leftSize - runHalf(leftSize, idx) - .flatMap { left => - val right = runHalf(rightSize, idx + leftSize) - G.map2Eval(left, right) { (_, _) => () } + G match { + case x: StackSafeMonad[G] => Traverse.traverse_Directly(fa)(f)(x) + case _ => + // the cost of this is O(size) + // c(n) = 1 + 2 * c(n/2) + // invariant: size >= 1 + def runHalf(size: Int, idx: Int): Eval[G[Unit]] = + if (size > 1) { + val leftSize = size / 2 + val rightSize = size - leftSize + runHalf(leftSize, idx) + .flatMap { left => + val right = runHalf(rightSize, idx + leftSize) + G.map2Eval(left, right) { (_, _) => () } + } + } else { + val a = fa(idx) + // we evaluate this at most one time, + // always is a bit cheaper in such cases + // + // Here is the point of the laziness using Eval: + // we avoid calling f(a) or G.void in the + // event that the computation has already + // failed. We do not use laziness to avoid + // traversing fa, which we will do fully + // in all cases. + Eval.always { + val gb = f(a) + G.void(gb) + } } - } else { - val a = fa(idx) - // we evaluate this at most one time, - // always is a bit cheaper in such cases - // - // Here is the point of the laziness using Eval: - // we avoid calling f(a) or G.void in the - // event that the computation has already - // failed. We do not use laziness to avoid - // traversing fa, which we will do fully - // in all cases. - Eval.always { - val gb = f(a) - G.void(gb) - } - } - val len = fa.length - if (len == 0) G.unit - else runHalf(len, 0).value + val len = fa.length + if (len == 0) G.unit + else runHalf(len, 0).value + } } override def mapAccumulate[S, A, B](init: S, fa: Vector[A])(f: (S, A) => (S, B)): (S, Vector[B]) = @@ -263,7 +270,11 @@ private[instances] trait VectorInstancesBinCompat0 { override def flattenOption[A](fa: Vector[Option[A]]): Vector[A] = fa.flatten def traverseFilter[G[_], A, B](fa: Vector[A])(f: (A) => G[Option[B]])(implicit G: Applicative[G]): G[Vector[B]] = - G.map(Chain.traverseFilterViaChain(fa)(f))(_.toVector) + G match { + case x: StackSafeMonad[G] => x.map(TraverseFilter.traverseFilterDirectly(fa)(f)(x))(_.toVector) + case _ => + G.map(Chain.traverseFilterViaChain(fa)(f))(_.toVector) + } override def filterA[G[_], A](fa: Vector[A])(f: (A) => G[Boolean])(implicit G: Applicative[G]): G[Vector[A]] = traverse diff --git a/kernel/src/main/scala-2.12/cats/kernel/compat/scalaVersionSpecific.scala b/kernel/src/main/scala-2.12/cats/kernel/compat/scalaVersionSpecific.scala index ee73d39ae2..ea473c1350 100644 --- a/kernel/src/main/scala-2.12/cats/kernel/compat/scalaVersionSpecific.scala +++ b/kernel/src/main/scala-2.12/cats/kernel/compat/scalaVersionSpecific.scala @@ -34,6 +34,8 @@ private[cats] object scalaVersionSpecific { implicit class traversableOnceExtension[A](private val to: TraversableOnce[A]) extends AnyVal { def iterator: Iterator[A] = to.toIterator + + def knownSize: Int = -1 } implicit class doubleExtension(private val double: Double) extends AnyVal { diff --git a/tests/shared/src/test/scala-2.13+/cats/tests/ArraySeqSuite.scala b/tests/shared/src/test/scala-2.13+/cats/tests/ArraySeqSuite.scala index 9c77c3cbdf..2d1b3296de 100644 --- a/tests/shared/src/test/scala-2.13+/cats/tests/ArraySeqSuite.scala +++ b/tests/shared/src/test/scala-2.13+/cats/tests/ArraySeqSuite.scala @@ -21,7 +21,7 @@ package cats.tests -import cats.{Align, Alternative, CoflatMap, Monad, MonoidK, Traverse, TraverseFilter} +import cats.{Align, Alternative, CoflatMap, Eval, Monad, MonoidK, Traverse, TraverseFilter} import cats.kernel.{Eq, Hash, Monoid, Order, PartialOrder} import cats.kernel.laws.discipline.{EqTests, HashTests, MonoidTests, OrderTests, PartialOrderTests} import cats.laws.discipline.{ @@ -52,7 +52,9 @@ class ArraySeqSuite extends CatsSuite { checkAll("ArraySeq[Int]", AlternativeTests[ArraySeq].alternative[Int, Int, Int]) checkAll("Alternative[ArraySeq]", SerializableTests.serializable(Alternative[ArraySeq])) + // Traverse behaviour discriminates on the Runtime type of the Applicative checkAll("ArraySeq[Int] with Option", TraverseTests[ArraySeq].traverse[Int, Int, Int, Set[Int], Option, Option]) + checkAll("ArraySeq[Int] with Eval", TraverseTests[ArraySeq].traverse[Int, Int, Int, Set[Int], Eval, Eval]) checkAll("Traverse[ArraySeq]", SerializableTests.serializable(Traverse[ArraySeq])) checkAll("ArraySeq[Int]", MonadTests[ArraySeq].monad[Int, Int, Int]) diff --git a/tests/shared/src/test/scala/cats/tests/ChainSuite.scala b/tests/shared/src/test/scala/cats/tests/ChainSuite.scala index ad5d593aa0..4a920ad4c4 100644 --- a/tests/shared/src/test/scala/cats/tests/ChainSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/ChainSuite.scala @@ -43,7 +43,9 @@ class ChainSuite extends CatsSuite { checkAll("Chain[Int]", AlternativeTests[Chain].alternative[Int, Int, Int]) checkAll("Alternative[Chain]", SerializableTests.serializable(Alternative[Chain])) + // Traverse behaviour discriminates on the Runtime type of the Applicative checkAll("Chain[Int] with Option", TraverseTests[Chain].traverse[Int, Int, Int, Set[Int], Option, Option]) + checkAll("Chain[Int] with Eval", TraverseTests[Chain].traverse[Int, Int, Int, Set[Int], Eval, Eval]) checkAll("Traverse[Chain]", SerializableTests.serializable(Traverse[Chain])) checkAll("Chain[Int]", MonadTests[Chain].monad[Int, Int, Int]) diff --git a/tests/shared/src/test/scala/cats/tests/ListSuite.scala b/tests/shared/src/test/scala/cats/tests/ListSuite.scala index 8bb06f2d22..d23c224fdb 100644 --- a/tests/shared/src/test/scala/cats/tests/ListSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/ListSuite.scala @@ -21,7 +21,7 @@ package cats.tests -import cats.{Align, Alternative, CoflatMap, Monad, Semigroupal, Traverse, TraverseFilter} +import cats.{Align, Alternative, CoflatMap, Eval, Monad, Semigroupal, Traverse, TraverseFilter} import cats.data.{NonEmptyList, ZipList} import cats.laws.discipline.{ AlignTests, @@ -52,7 +52,9 @@ class ListSuite extends CatsSuite { checkAll("List[Int]", AlternativeTests[List].alternative[Int, Int, Int]) checkAll("Alternative[List]", SerializableTests.serializable(Alternative[List])) + // Traverse behaviour discriminates on the Runtime type of the Applicative checkAll("List[Int] with Option", TraverseTests[List].traverse[Int, Int, Int, Set[Int], Option, Option]) + checkAll("List[Int] with Eval", TraverseTests[List].traverse[Int, Int, Int, Set[Int], Eval, Eval]) checkAll("Traverse[List]", SerializableTests.serializable(Traverse[List])) checkAll("List[Int]", MonadTests[List].monad[Int, Int, Int]) diff --git a/tests/shared/src/test/scala/cats/tests/MapSuite.scala b/tests/shared/src/test/scala/cats/tests/MapSuite.scala index 21b80e6b92..495ff4b015 100644 --- a/tests/shared/src/test/scala/cats/tests/MapSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/MapSuite.scala @@ -21,7 +21,7 @@ package cats.tests -import cats.{Align, FlatMap, FunctorFilter, MonoidK, Semigroupal, Show, UnorderedTraverse} +import cats.{Align, Eval, FlatMap, FunctorFilter, MonoidK, Semigroupal, Show, UnorderedTraverse} import cats.arrow.Compose import cats.kernel.{CommutativeMonoid, Monoid} import cats.kernel.instances.StaticMethods.wrapMutableMap @@ -48,9 +48,11 @@ class MapSuite extends CatsSuite { checkAll("Map[Int, Int]", FlatMapTests[Map[Int, *]].flatMap[Int, Int, Int]) checkAll("FlatMap[Map[Int, *]]", SerializableTests.serializable(FlatMap[Map[Int, *]])) + // Traverse behaviour discriminates on the Runtime type of the Applicative checkAll("Map[Int, Int] with Option", UnorderedTraverseTests[Map[Int, *]].unorderedTraverse[Int, Int, Int, Option, Option] ) + checkAll("Map[Int, Int] with Eval", UnorderedTraverseTests[Map[Int, *]].unorderedTraverse[Int, Int, Int, Eval, Eval]) checkAll("UnorderedTraverse[Map[Int, *]]", SerializableTests.serializable(UnorderedTraverse[Map[Int, *]])) checkAll("Map[Int, Int]", FunctorFilterTests[Map[Int, *]].functorFilter[Int, Int, Int]) diff --git a/tests/shared/src/test/scala/cats/tests/QueueSuite.scala b/tests/shared/src/test/scala/cats/tests/QueueSuite.scala index 670eac7a98..40ae944294 100644 --- a/tests/shared/src/test/scala/cats/tests/QueueSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/QueueSuite.scala @@ -21,7 +21,7 @@ package cats.tests -import cats.{Alternative, CoflatMap, Monad, Semigroupal, Traverse, TraverseFilter} +import cats.{Alternative, CoflatMap, Eval, Monad, Semigroupal, Traverse, TraverseFilter} import cats.laws.discipline.{ AlternativeTests, CoflatMapTests, @@ -50,7 +50,9 @@ class QueueSuite extends CatsSuite { checkAll("Queue[Int]", MonadTests[Queue].monad[Int, Int, Int]) checkAll("Monad[Queue]", SerializableTests.serializable(Monad[Queue])) + // Traverse behaviour discriminates on the Runtime type of the Applicative checkAll("Queue[Int] with Option", TraverseTests[Queue].traverse[Int, Int, Int, Set[Int], Option, Option]) + checkAll("Queue[Int] with Eval", TraverseTests[Queue].traverse[Int, Int, Int, Set[Int], Eval, Eval]) checkAll("Traverse[Queue]", SerializableTests.serializable(Traverse[Queue])) checkAll("Queue[Int]", TraverseFilterTests[Queue].traverseFilter[Int, Int, Int]) diff --git a/tests/shared/src/test/scala/cats/tests/SeqSuite.scala b/tests/shared/src/test/scala/cats/tests/SeqSuite.scala index e2d7106b5a..0d302fed15 100644 --- a/tests/shared/src/test/scala/cats/tests/SeqSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/SeqSuite.scala @@ -21,7 +21,7 @@ package cats.tests -import cats.{Align, Alternative, CoflatMap, Monad, Semigroupal, Traverse, TraverseFilter} +import cats.{Align, Alternative, CoflatMap, Eval, Monad, Semigroupal, Traverse, TraverseFilter} import cats.data.ZipSeq import cats.laws.discipline.{ AlignTests, @@ -51,7 +51,9 @@ class SeqSuite extends CatsSuite { checkAll("Seq[Int]", AlternativeTests[Seq].alternative[Int, Int, Int]) checkAll("Alternative[Seq]", SerializableTests.serializable(Alternative[Seq])) + // Traverse behaviour discriminates on the Runtime type of the Applicative checkAll("Seq[Int] with Option", TraverseTests[Seq].traverse[Int, Int, Int, Set[Int], Option, Option]) + checkAll("Seq[Int] with Eval", TraverseTests[Seq].traverse[Int, Int, Int, Set[Int], Eval, Eval]) checkAll("Traverse[Seq]", SerializableTests.serializable(Traverse[Seq])) checkAll("Seq[Int]", MonadTests[Seq].monad[Int, Int, Int]) diff --git a/tests/shared/src/test/scala/cats/tests/VectorSuite.scala b/tests/shared/src/test/scala/cats/tests/VectorSuite.scala index f4b91e7369..4777dab5b1 100644 --- a/tests/shared/src/test/scala/cats/tests/VectorSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/VectorSuite.scala @@ -21,7 +21,7 @@ package cats.tests -import cats.{Align, Alternative, CoflatMap, Monad, Semigroupal, Traverse, TraverseFilter} +import cats.{Align, Alternative, CoflatMap, Eval, Monad, Semigroupal, Traverse, TraverseFilter} import cats.data.{NonEmptyVector, ZipVector} import cats.laws.discipline.{ AlignTests, @@ -51,7 +51,9 @@ class VectorSuite extends CatsSuite { checkAll("Vector[Int]", AlternativeTests[Vector].alternative[Int, Int, Int]) checkAll("Alternative[Vector]", SerializableTests.serializable(Alternative[Vector])) + // TraverseFilter behaviour discriminates on the Runtime type of the Applicative checkAll("Vector[Int] with Option", TraverseTests[Vector].traverse[Int, Int, Int, Set[Int], Option, Option]) + checkAll("Vector[Int] with Eval", TraverseTests[Vector].traverse[Int, Int, Int, Set[Int], Eval, Eval]) checkAll("Traverse[Vector]", SerializableTests.serializable(Traverse[Vector])) checkAll("Vector[Int]", MonadTests[Vector].monad[Int, Int, Int])