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
Optimize traverse #4498
Optimize traverse #4498
Changes from 15 commits
858cca4
847df54
878c4d0
9d86c98
c4bcba2
c4c64ec
1344a70
b6973dc
f0648c4
dbccc8e
fbf5ea5
9f60d2b
f4fcf57
d50bf83
5c5049f
a1c9ff5
8cc742e
e3ae584
f95b087
c5d90a6
2d5f4d7
702ab8b
b08196e
92c91e4
7d615dc
eebed5b
803107d
0ee49e6
6529be7
a62ce4c
6cb787d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,6 +23,9 @@ package cats | |
|
||
import cats.data.State | ||
import cats.data.StateT | ||
import cats.kernel.compat.scalaVersionSpecific._ | ||
import cats.StackSafeMonad | ||
import scala.collection.mutable | ||
|
||
/** | ||
* Traverse, also known as Traversable. | ||
|
@@ -284,4 +287,35 @@ object Traverse { | |
@deprecated("Use cats.syntax object imports", "2.2.0") | ||
object nonInheritedOps extends ToTraverseOps | ||
|
||
private[cats] def traverseDirectly[Coll[x] <: IterableOnce[x], G[_], A, B]( | ||
builder: mutable.Builder[B, Coll[B]] | ||
)(fa: IterableOnce[A])(f: A => G[B])(implicit G: StackSafeMonad[G]): G[Coll[B]] = { | ||
val size = fa.knownSize | ||
if (size >= 0) { | ||
builder.sizeHint(size) | ||
} | ||
G.map(fa.iterator.foldLeft(G.pure(builder)) { case (accG, a) => | ||
G.flatMap(accG) { acc => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add a comment why not Since we know G is lazy (a StackSafeMonad has to be I think), I'm not sure the downside here. One answer would be we don't have to call By calling map2 we are at least communicating to G what we are doing, and in principle, some monads could optimize this (e.g. a Parser can make a more optimized map2 than flatMap, and it can also be StackSafe since runs lazily only when input is passed to the resulting parser). |
||
G.map(f(a)) { a => | ||
acc += a | ||
acc | ||
} | ||
} | ||
})(_.result()) | ||
} | ||
|
||
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.map(iter.foldLeft(f(first)) { case (g, a) => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be G.void(.. so that has a chance for an optimized implementation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, good catch! |
||
G.flatMap(g) { _ => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was no good reason 😅 Thanks, I'll change it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Maybe this is something we should consider though? Edit: ahh, I see your comment on f95b087. Fair enough. |
||
f(a) | ||
} | ||
})(_ => ()) | ||
} else G.unit | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,8 +22,10 @@ | |
package cats | ||
|
||
import cats.data.State | ||
import cats.kernel.compat.scalaVersionSpecific._ | ||
|
||
import scala.collection.immutable.{IntMap, TreeSet} | ||
import scala.collection.mutable | ||
|
||
/** | ||
* `TraverseFilter`, also known as `Witherable`, represents list-like structures | ||
|
@@ -203,4 +205,21 @@ object TraverseFilter { | |
@deprecated("Use cats.syntax object imports", "2.2.0") | ||
object nonInheritedOps extends ToTraverseFilterOps | ||
|
||
private[cats] def traverseFilterDirectly[Coll[x] <: IterableOnce[x], G[_], A, B]( | ||
builder: mutable.Builder[B, Coll[B]] | ||
)(fa: IterableOnce[A])(f: A => G[Option[B]])(implicit G: StackSafeMonad[G]): G[Coll[B]] = { | ||
val size = fa.knownSize | ||
if (size >= 0) { | ||
builder.sizeHint(size) | ||
} | ||
G.map(fa.iterator.foldLeft(G.pure(builder)) { case (bldrG, a) => | ||
G.flatMap(bldrG) { bldr => | ||
G.map(f(a)) { | ||
case Some(b) => bldr += b | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't see why we know this is safe or lawful. We have a mutable data structure that could potentially be in a multithreaded situation with G. Also, consider cases like IO where you have a long computation that finally fails, then you recover some part of it to succeed. It feels like this mutable builder could remember things from failed branches. I think using an immutable builder (like for instance just building up a List, Chain or Vector) would be much easier to verify it is lawful. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It can't be: every append to the builder happens in
Hmm. I'm not entirely sure how this applies to
Won't disagree here. We could do a benchmark to see how much performance we are leaving on the table with that strategy. |
||
case None => bldr | ||
} | ||
} | ||
})(_.result()) | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1241,11 +1241,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 { | ||
johnynek marked this conversation as resolved.
Show resolved
Hide resolved
|
||
case x: StackSafeMonad[G] => | ||
x.map(Traverse.traverseDirectly(List.newBuilder[B])(fa.iterator)(f)(x))(Chain.fromSeq(_)) | ||
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 { | ||
johnynek marked this conversation as resolved.
Show resolved
Hide resolved
|
||
case x: StackSafeMonad[G] => Traverse.traverse_Directly(fa.iterator)(f)(x) | ||
case _ => | ||
foldRight(fa, Always(G.pure(()))) { (a, acc) => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
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) | ||
|
@@ -1354,11 +1370,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] => | ||
G.map(TraverseFilter.traverseFilterDirectly(List.newBuilder[B])(fa.iterator)(f)(x))(Chain.fromSeq(_)) | ||
case _ => | ||
johnynek marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.