-
-
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 NonEmptyList#partitionE #1858
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1858 +/- ##
=========================================
+ Coverage 95.17% 95.2% +0.02%
=========================================
Files 248 248
Lines 4352 4379 +27
Branches 121 125 +4
=========================================
+ Hits 4142 4169 +27
Misses 210 210
Continue to review full report at Codecov.
|
Would it be more useful to add it to def partitionIor[B, C](f: A => Ior[B, C]): Ior[NonEmptyList[B], NonEmptyList[C]] = { |
You mean generalizing it to def partitionIor[F[_]: NonEmptyTraverse, A, B, C](fa: F[A])(f: A => Ior[B, C]): Ior[F[B], F[C]] Not sure, if that works, but would be cool :) |
@LukaJCB i didn't mean to generalize the result |
You don't need traverse do you? Reducible will do, no? |
@johnynek is correct! I was able to generalize it to Reducible :) |
2f25872
to
3be4721
Compare
looks good. is it just me or does anyone else feel a bit weird that we have a like how about this def partitionEither[A, B, C](fa: F[A])(f: A => Either[B, C])(implicit F: MonoidK[F], A: Applicative[F]): (F[B], F[C]) =
foldRight(fa, Eval.later((F.empty[B], F.empty[C]))) { (a, e) =>
e.map {
case (fbs, fcs) => f(a).fold(
b => (F.combineK(A.pure(b), fbs), fcs),
c => (fbs, F.combineK(A.pure(c), fcs))
)
}
}.value
} Note that it requires |
I don't think we can reduce the requirements, because either side of the tuple might be empty, so MonoidK is a must, and for Applicative to go, we'd need a way to go from |
I am fine with keeping those requirements. My guess is that for most of the use cases they are going to have those two instances anyway. |
That |
That's a good idea! It also just dawned on me that def mapSeparate[A, B, C](fa: F[A])(f: A => Either[B, C])(implicit A: Alternative[F]): (F[B], F[C]) =
foldMap(fa)(a => f(a) match {
case Right(c) => (A.empty[B], A.pure(c))
case Left(b) => (A.pure(b), A.empty[C])
}) |
Right I keep forgetting |
It seems the above didn't work, because there's no def mapSeparate[A, B, C](fa: F[A])(f: A => Either[B, C])(implicit A: Alternative[F]): (F[B], F[C]) = {
implicit val mFbFC: Monoid[(F[B], F[C])] = new Monoid[(F[B], F[C])] {
def empty: (F[B], F[C]) = (A.empty[B], A.empty[C])
def combine(x: (F[B], F[C]), y: (F[B], F[C])): (F[B], F[C]) = (x, y) match {
case ((xfb, xfc), (yfb, yfc)) => (MonoidK[F].combineK(xfb, yfb), MonoidK[F].combineK(xfc, yfc))
}
}
foldMap(fa)(a => f(a) match {
case Right(c) => (A.empty[B], A.pure(c))
case Left(b) => (A.pure(b), A.empty[C])
})
} |
can |
|
current PR works for me. |
case Left(b) => (pure(b), empty[C]) | ||
}) | ||
} | ||
|
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 think @peterneyens was suggesting that
you can just write
import cats.instances.either._
def mapSeparate[A, B, C](fa: F[A])(f: A => Either[B, C])(implicit FM: Monad[F]): (F[B], F[C]) =
separate(map(fa)(f))
So instead of requiring F
to be Foldable
, it requires to be a Monad
.
Or we can use your implementation, which requires Foldable
, which might be more performant? (traverse once instead three times?)
I still think we should name it partitionEither
or something, the idea is to provide a familiar api to users. map
and then separate
probably don't justify a special delegate method also named mapSeparate
.
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 think I prefer the Foldable
way, for the reasons you mentioned. I'm not married to the name, but it makes sense in the same way flatMap
makes sense, I guess.
I think I'd call the method on Foldable
partitionEither
as you suggested and renamed the one on Reducible
to partitionIor
Maybe?
partitionE
isn't a great name IMO, because there's no real precedence for the "E" and I think if we want to stay a beginner friendly library we should have as little of these one-letter-abbreviations as possible.
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.
The current implementation is better than map
+ separate
indeed. Just wanted to point out the similarity.
separate
is available for all Bifoldable
s while we currently have to use Either
with mapSeparate
:
- With
separate
you could also splitIor
andTuple2
(similar tounzip
). mapSeparate
+toEither
will probably be faster thanmap(toEither).separate
as you fold once instead of twice.
I agree that partitionE
isn't a great name.
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 2 cents
- since we are going to use
Foldable
, it seems to make more sense to be onFoldable
thanAlternative
. Having a pair of partition methods onFoldable
andReducible
is what I was looking for. - both methods should be named
partitionEitherXX
since they both use a function that returns an Either for the actual partition logic. Maybe we can callReducible#partitionEitherNE
andFoldable#partitionEither
- maybe we should mention the
map
+separate
option in the scaladoc inFoldable#partitionEither
, I think it might worth some promotion.
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'm +1 on renaming to Foldable#partitionEither
and mentioning the map
+ separate
option in the scaladoc. Disagree on partitionEitherNE
though, for similar reasons as partitionE
.
How about nonEmptyPartition
? Would go in line with nonEmptyIntercalate
etc. 🤔
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 am fine with nonEmptyPartition
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.
Renamed to Foldable#partitionEither
and Reducible#nonEmptyPartition
:)
val h: Int => Either[String, Double] = f andThen Left.apply | ||
|
||
val withG = fi.partitionE(g).fold(_ => NonEmptyList.one(""), identity, (l,r) => r) | ||
withG should === (Reducible[F].toNonEmptyList((fi.map(f)))) |
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.
If this map
is the reason we add the Functor
constraint to ReducibleCheck
we could write Reducible[F].toNonEmptyList(fi).map(f)
, right?
val g: Int => Either[Double, String] = f andThen Right.apply | ||
val h: Int => Either[String, Double] = f andThen Left.apply | ||
|
||
val withG = fi.partitionE(g).fold(_ => NonEmptyList.one(""), identity, (l,r) => r) |
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.
The fold
could be .right.getOrElse(NonEmptyList.one(""))
.
case Left(b) => (pure(b), empty[C]) | ||
}) | ||
} | ||
|
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.
The current implementation is better than map
+ separate
indeed. Just wanted to point out the similarity.
separate
is available for all Bifoldable
s while we currently have to use Either
with mapSeparate
:
- With
separate
you could also splitIor
andTuple2
(similar tounzip
). mapSeparate
+toEither
will probably be faster thanmap(toEither).separate
as you fold once instead of twice.
I agree that partitionE
isn't a great name.
import cats.syntax.either._ | ||
|
||
def g(a: A, eval: Eval[Ior[NonEmptyList[B], NonEmptyList[C]]]): Eval[Ior[NonEmptyList[B], NonEmptyList[C]]] = { | ||
val ior = eval.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.
We should probably do:
eval.map { ior =>
(f(a), ior) match {
// ...
}
}
d40c925
to
1f30a1c
Compare
Thanks for your review @peterneyens! I addressed your comments :) |
@@ -25,6 +25,40 @@ abstract class FoldableCheck[F[_]: Foldable](name: String)(implicit ArbFInt: Arb | |||
} | |||
} | |||
|
|||
test("Alternative#partitionEither retains size") { |
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.
titles of the tests should be Foldable#partitionEither xxxx
@@ -407,6 +407,21 @@ import simulacrum.typeclass | |||
}.toList | |||
|
|||
/** | |||
* Separate this Foldable into a Tuple by a separating function `A => Either[B, C]` | |||
*/ | |||
def partitionEither[A, B, C](fa: F[A])(f: A => Either[B, C])(implicit F: Foldable[F], A: Alternative[F]): (F[B], F[C]) = { |
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.
you are already in Foldable
, so no need for the implicit F: Foldable[F]
here
d00a55f
to
c7f3b69
Compare
} | ||
} | ||
|
||
test("Foldable#mapSeparate remains sorted") { |
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.
You forgot to rename it here.
@@ -407,6 +407,31 @@ import simulacrum.typeclass | |||
}.toList | |||
|
|||
/** | |||
* Separate this Foldable into a Tuple by a separating function `A => Either[B, C]` | |||
* Equivalent to `Functor#map` and then `Alternative#separate`. |
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.
If we did define it in Alternative
, we could add a consistency law checking this equality, but hey ... you can't always get what you want 😃
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.
True, I don't really mind either way tbh.
We could add a test that checks if it's consistent with List#partition though 😄
@LukaJCB since I wrote this, I'd appreciate a mention in the method 😉 recall that my original implementation carried no license with it. |
hmm, on second thoughts, your impl is sufficiently different that I don't consider it a derivative works, no attribution required. |
Hey @fommil sorry for any confusion, didn't mean any harm! |
@peterneyens any other feedback? |
* res1: cats.data.Ior[cats.data.NonEmptyList[Nothing],cats.data.NonEmptyList[Int]] = Right(NonEmptyList(4, 8, 12, 16)) | ||
* }}} | ||
*/ | ||
def nonEmptyPartition[A, B, C](fa: F[A])(f: A => Either[B, C]): Ior[NonEmptyList[B], NonEmptyList[C]] = { |
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.
couldn't we do this considerably more efficiently for NonEmptyList and NonEmptyVector? Can we add methods to those datatypes that does so? Eval is kind of a perf killer, sadly, which this method hides.
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 just had the same idea :D I already implemented a O(n) solution for partitionEither
on List
and will override nonemptyPartition as well!
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.
Added overrides for NEL and NEV :)
dcb2852
to
505475d
Compare
} | ||
} | ||
|
||
test("Reducible#nonEmptyPartition remains sorted") { |
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.
this is a bit weird. The only F[_]
part of it toNonEmptyList
. After that, we are testing nonEmptyPartition
on NonEmptyList, no?
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.
Yes that's true, I wrote this test when originally nonEmptyPartition
was part of NonEmptyList
. I could move the test there, if you'd like :) Otherwise, I'm not sure it's possible to sort an F[_]
with just Reducible
07d5a6a
to
e1d39b1
Compare
👍 |
Great contribution. Thanks @LukaJCB ! |
As per #1743 (now closed). Also open for alternative names (
partitionIor
maybe?)