Skip to content

Commit

Permalink
Adding partitionEither
Browse files Browse the repository at this point in the history
  • Loading branch information
Devon Stewart committed Dec 28, 2018
1 parent 76704df commit db97a61
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 0 deletions.
29 changes: 29 additions & 0 deletions core/src/main/scala/cats/Foldable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
*/
Expand Down
53 changes: 53 additions & 0 deletions tests/src/test/scala/cats/tests/FoldableSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit db97a61

Please sign in to comment.