-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Add TraverseFilter.mapAccumulateFilter #4561
base: main
Are you sure you want to change the base?
Conversation
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.
Thanks for taking the time!
I think if we add this we should make it fast for as many instances in cats that we can.
@@ -122,6 +122,9 @@ trait TraverseFilter[F[_]] extends FunctorFilter[F] { | |||
override def mapFilter[A, B](fa: F[A])(f: A => Option[B]): F[B] = | |||
traverseFilter[Id, A, B](fa)(f) | |||
|
|||
def mapAccumulateFilter[S, A, B](init: S, fa: F[A])(f: (S, A) => (S, Option[B])): (S, F[B]) = | |||
traverseFilter(fa)(a => State(s => f(s, a))).run(init).value |
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.
Can we override this for some of the built in collections? State is rather slow so we should avoid it for List, Vector, Chain, NonEmptyList, NonEmptyVector, ...
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.
I can copy StaticMethods.mapAccumulateFromStrictFunctor
for mapAccumulateFilter
, this will cover for some collections
There is no override of |
Same for |
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.
Thanks for sending this PR
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.
Bikeshed question: why mapAccumulateFilter
vs mapFilterAccumulate
? I think I prefer the latter.
I just added "Filter"-suffix after original method name from I have no opinion here, I don't mind both |
I think it can be addressed in a separate follow-up PR, just to avoid blowing up the scope of this one. |
We just get to decide, whether the new method is likely As a bit more crazy idea (maybe not that crazy though): we can add two functions instead: def mapAccumulateFilter[S, A, B](init: S, fa: F[A])(f: (S, A) => (S, Option[B])): (S, F[B])
def mapFilterAccumulate[S, A, B](init: S, fa: F[A])(f: (S, A) => Option[(S, B)]): (S, F[B]) The former always does accumulate regardless of filter, hence the name. Of course, we can implement def mapFilterAccumulate[S, A, B](init: S, fa: F[A])(f: (S, A) => Option[(S, B)]): (S, F[B]) =
mapAccumulateFilter(init, fa) { (s1, a) =>
f(s1, a) match {
case (s2, Some(b)) => Some((s2, b))
case (_, None) => None // drop `S` from this iteration
}
} However both functions can come handy under certain circumstances. Moreover, even more important – it could help up to avoid choosing between two really good names and just move on with this PR :) |
This is implementation of f(s1, a) match
case Some((s2, b)) => (s2, Some(b))
case None => (s1, None) |
Yes, thank you 😊 Anyway, @armanbilge – wdyt? |
@@ -38,6 +38,19 @@ abstract class TraverseFilterSuite[F[_]: TraverseFilter](name: String)(implicit | |||
|
|||
implicit def T: Traverse[F] = implicitly[TraverseFilter[F]].traverse | |||
|
|||
test(s"TraverseFilter[$name].mapAccumulateFilter") { |
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.
Looks like it does not check the default implementation (based on State
), does it?
Except maybe one for Stream
, but I wouldn't count on it.
For testing default implementations we usually use ListWrapper
from testkit:
ListWrapper.scala
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.
I had straightforwardly copy-pasted and updated tests from mapAccumulate
here:
cats/tests/shared/src/test/scala/cats/tests/TraverseSuite.scala
Lines 45 to 56 in 70dbf8f
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))) | |
val rhs = fa.foldLeft((init, List.empty[Int])) { case ((s1, acc), a) => | |
val (s2, b) = fn((s1, a)) | |
(s2, b :: acc) | |
} | |
assert(lhs.map(_.toList) === rhs.map(_.reverse)) | |
} | |
} |
If that tests doesn't test default implementation too, I can update mapAccumulateFilter
tests. Can you provide an example of how to pass ListWrapper
?
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.
For testing default implementations we usually use
ListWrapper
from testkit: ListWrapper.scala
I am failing to understand how to use ListWrapper
to test mapAccumulateFilter
. I was looking for examples in other Suites, but it is either suites for data and not typeclasses (OptionT
, Try
, etc.), or it refers to <Typeclass>Tests[ListWrapper].<methodToTest>
, like in the ApplicativeSuite
:
cats/tests/shared/src/test/scala/cats/tests/ApplicativeSuite.scala
Lines 102 to 104 in 70dbf8f
implicit val listwrapperApplicative: Applicative[ListWrapper] = ListWrapper.applicative | |
implicit val listwrapperCoflatMap: CoflatMap[ListWrapper] = Applicative.coflatMap[ListWrapper] | |
checkAll("Applicative[ListWrapper].coflatMap", CoflatMapTests[ListWrapper].coflatMap[String, String, String]) |
which I can not apply here, because we don't have TraverseFilter[?].mapAccumulateFilter
. Am I missing something?
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.
My apologies for the delay – I was snowed under a bit. Actually, there's TraverseFilter
for ListWrapper
:
cats/testkit/src/main/scala/cats/tests/ListWrapper.scala
Lines 80 to 90 in 47fbad7
val traverseFilter: TraverseFilter[ListWrapper] = { | |
val F = TraverseFilter[List] | |
new TraverseFilter[ListWrapper] { | |
def traverse = ListWrapper.traverse | |
def traverseFilter[G[_], A, B]( | |
fa: ListWrapper[A] | |
)(f: A => G[Option[B]])(implicit G: Applicative[G]): G[ListWrapper[B]] = | |
G.map(F.traverseFilter(fa.list)(f))(ListWrapper.apply) | |
} | |
} |
To test the default implementation you can either call it directly:
ListWrapper.traverseFilter.mapAccumulateFilter(...)
or make it an implicit in the scope:
implicit val listWrapperTraverseFilter: TraverseFilter[ListWrapper] = ListWrapper.traverseFilter
And then you can work with TraverseFilter
for ListWrapper
as usual.
Hi there, just a check-up: where do we stand on this PR? |
If
Traverse
hasmapAccumulate
, why don'tTraverseFilter
hasmapAccumulateFilter
?Now it has. I implement method itself (almost fully copied from
mapAccumulate
, changingtraverse
totraverseFilter
), syntax ops, and tests (almost fully copied frommapAccumulate
, adjustingfn
signature)