From db97a61d6029f5ce195f4c0a470ad80dbf364d27 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Sat, 24 Nov 2018 13:29:22 -0800 Subject: [PATCH] Adding partitionEither --- core/src/main/scala/cats/Foldable.scala | 29 ++++++++++ .../test/scala/cats/tests/FoldableSuite.scala | 53 +++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/core/src/main/scala/cats/Foldable.scala b/core/src/main/scala/cats/Foldable.scala index f4256113027..7ae1a51aeb9 100644 --- a/core/src/main/scala/cats/Foldable.scala +++ b/core/src/main/scala/cats/Foldable.scala @@ -506,6 +506,35 @@ import Foldable.sentinel ) } + /** + * Separate this Foldable into a Tuple by an effectful separating function `A => G[Either[B, C]]` + * Equivalent to `Bitraversable#traverse` over `Alternative#separate` + * + * {{{ + * scala> import cats.implicits._ + * scala> val list = List(1,2,3,4) + * scala> Foldable[List].partitionEitherM(list)(a => if (a % 2 == 0) Eval.now(Left(a.toString)) else Eval.now(Right(a))).value + * res0: (List[String], List[Int]) = (List(2, 4),List(1, 3)) + * scala> Foldable[List].partitionEitherM(list)(a => Eval.later(Either.right(a * 4))).value + * res1: (List[Nothing], List[Int]) = (List(),List(4, 8, 12, 16)) + * }}} + */ + def partitionEitherM[G[_], A, B, C](fa: F[A])(f: A => G[Either[B, C]])(implicit A: Alternative[F], + M: Monad[G]): G[(F[B], F[C])] = { + import cats.instances.tuple._ + + implicit val mb: Monoid[F[B]] = A.algebra[B] + implicit val mc: Monoid[F[C]] = A.algebra[C] + + foldMapM[G, A, (F[B], F[C])](fa)( + a => + M.map(f(a)) { + case Right(c) => (A.empty[B], A.pure(c)) + case Left(b) => (A.pure(b), A.empty[C]) + } + ) + } + /** * Convert F[A] to a List[A], only including elements which match `p`. */ diff --git a/tests/src/test/scala/cats/tests/FoldableSuite.scala b/tests/src/test/scala/cats/tests/FoldableSuite.scala index 2521f303942..9c12c1f4348 100644 --- a/tests/src/test/scala/cats/tests/FoldableSuite.scala +++ b/tests/src/test/scala/cats/tests/FoldableSuite.scala @@ -78,6 +78,59 @@ abstract class FoldableSuite[F[_]: Foldable](name: String)(implicit ArbFInt: Arb } } + test("Foldable#partitionEitherM retains size") { + forAll { (fi: F[Int], f: Int => Either[String, String]) => + val vector = Foldable[F].toList(fi).toVector + val result = Foldable[Vector].partitionEitherM(vector)(f.andThen(Option.apply)).map { + case (lefts, rights) => + (lefts <+> rights).size.toLong + } + result should ===(Option(fi.size)) + } + } + + test("Foldable#partitionEitherM consistent with List#partition") { + forAll { (fi: F[Int], f: Int => Either[String, String]) => + val list = Foldable[F].toList(fi) + val partitioned = Foldable[List].partitionEitherM(list)(f.andThen(Option.apply)) + val (ls, rs) = list + .map(f) + .partition({ + case Left(_) => true + case Right(_) => false + }) + + partitioned.map(_._1.map(_.asLeft[String])) should ===(Option(ls)) + partitioned.map(_._2.map(_.asRight[String])) should ===(Option(rs)) + } + } + + test("Foldable#partitionEitherM to one side is identity") { + forAll { (fi: F[Int], f: Int => String) => + val list = Foldable[F].toList(fi) + val g: Int => Option[Either[Double, String]] = f.andThen(Right.apply).andThen(Option.apply) + val h: Int => Option[Either[String, Double]] = f.andThen(Left.apply).andThen(Option.apply) + + val withG = Foldable[List].partitionEitherM(list)(g).map(_._2) + withG should ===(Option(list.map(f))) + + val withH = Foldable[List].partitionEitherM(list)(h).map(_._1) + withH should ===(Option(list.map(f))) + } + } + + test("Foldable#partitionEitherM remains sorted") { + forAll { (fi: F[Int], f: Int => Either[String, String]) => + val list = Foldable[F].toList(fi) + + val sorted = list.map(f).sorted + val pairs = Foldable[List].partitionEitherM(sorted)(Option.apply) + + pairs.map(_._1.sorted) should ===(pairs.map(_._1)) + pairs.map(_._2.sorted) should ===(pairs.map(_._2)) + } + } + test(s"Foldable[$name] summation") { forAll { (fa: F[Int]) => val total = iterator(fa).sum