diff --git a/core/src/main/scala/cats/Reducible.scala b/core/src/main/scala/cats/Reducible.scala index 348c037ca13..745023e6722 100644 --- a/core/src/main/scala/cats/Reducible.scala +++ b/core/src/main/scala/cats/Reducible.scala @@ -1,7 +1,6 @@ package cats -import cats.data.NonEmptyList - +import cats.data.{Ior, NonEmptyList} import simulacrum.typeclass /** @@ -177,6 +176,22 @@ import simulacrum.typeclass Reducible[NonEmptyList].reduce(NonEmptyList(hd, a :: intersperseList(tl, a))) } + def partitionE[A, B, C](fa: F[A])(f: A => Either[B, C]): Ior[NonEmptyList[B], NonEmptyList[C]] = { + 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 + (f(a), ior) match { + case (Right(c), Ior.Left(_)) => Eval.now(ior.putRight(NonEmptyList.one(c))) + case (Right(c), _) => Eval.now(ior.map(c :: _)) + case (Left(b), Ior.Right(r)) => Eval.now(Ior.bothNel(b, r)) + case (Left(b), _) => Eval.now(ior.leftMap(b :: _)) + } + } + + reduceRightTo(fa)(a => f(a).bimap(NonEmptyList.one, NonEmptyList.one).toIor)(g).value + } + override def isEmpty[A](fa: F[A]): Boolean = false override def nonEmpty[A](fa: F[A]): Boolean = true diff --git a/core/src/main/scala/cats/data/NonEmptyList.scala b/core/src/main/scala/cats/data/NonEmptyList.scala index 0a87272785f..4de780eccad 100644 --- a/core/src/main/scala/cats/data/NonEmptyList.scala +++ b/core/src/main/scala/cats/data/NonEmptyList.scala @@ -320,20 +320,6 @@ final case class NonEmptyList[+A](head: A, tail: List[A]) { b.result } - def partitionE[B, C](f: A => Either[B, C]): Ior[NonEmptyList[B], NonEmptyList[C]] = { - import cats.syntax.either._ - - val reversed = reverse - val lastIor = f(reversed.head).bimap(NonEmptyList.one, NonEmptyList.one).toIor - - reversed.tail.foldLeft(lastIor)((ior, a) => (f(a), ior) match { - case (Right(c), Ior.Left(l)) => ior.putRight(NonEmptyList.one(c)) - case (Right(c), _) => ior.map(c :: _) - case (Left(b), Ior.Right(r)) => Ior.bothNel(b, r) - case (Left(b), _) => ior.leftMap(b :: _) - }) - } - } object NonEmptyList extends NonEmptyListInstances { diff --git a/tests/src/test/scala/cats/tests/NonEmptyListTests.scala b/tests/src/test/scala/cats/tests/NonEmptyListTests.scala index 11f7bc6f3ff..991b4a5a29f 100644 --- a/tests/src/test/scala/cats/tests/NonEmptyListTests.scala +++ b/tests/src/test/scala/cats/tests/NonEmptyListTests.scala @@ -74,36 +74,6 @@ class NonEmptyListTests extends CatsSuite { } } - test("NonEmptyList#partitionE retains size") { - forAll { (nel: NonEmptyList[Int], f: Int => Either[String, String]) => - val folded = nel.partitionE(f).fold(identity, identity, _ ++ _.toList) - folded.size should === (nel.size) - } - } - - test("NonEmptyList#partitionE to one side is identity") { - forAll { (nel: NonEmptyList[Int], f: Int => String) => - val g: Int => Either[Double, String] = f andThen Right.apply - val h: Int => Either[String, Double] = f andThen Left.apply - - val withG = nel.partitionE(g).fold(_ => NonEmptyList.one(""), identity, (l,r) => r) - withG should === (nel.map(f)) - - val withH = nel.partitionE(h).fold(identity, _ => NonEmptyList.one(""), (l,r) => l) - withH should === (nel.map(f)) - } - } - - test("NonEmptyList#partitionE remains sorted") { - forAll { (nel: NonEmptyList[Int], f: Int => Either[String, String]) => - - val sorted = nel.map(f).sorted - val ior = sorted.partitionE(identity) - - ior.left.map(xs => xs.sorted should === (xs)) - ior.right.map(xs => xs.sorted should === (xs)) - } - } test("NonEmptyList#filter is consistent with List#filter") { forAll { (nel: NonEmptyList[Int], p: Int => Boolean) => diff --git a/tests/src/test/scala/cats/tests/ReducibleTests.scala b/tests/src/test/scala/cats/tests/ReducibleTests.scala index 8f6230a82ea..1a8a1163c4a 100644 --- a/tests/src/test/scala/cats/tests/ReducibleTests.scala +++ b/tests/src/test/scala/cats/tests/ReducibleTests.scala @@ -71,7 +71,7 @@ class ReducibleTestsAdditional extends CatsSuite { } -abstract class ReducibleCheck[F[_]: Reducible](name: String)(implicit ArbFInt: Arbitrary[F[Int]], ArbFString: Arbitrary[F[String]]) extends FoldableCheck[F](name) { +abstract class ReducibleCheck[F[_]: Reducible: Functor](name: String)(implicit ArbFInt: Arbitrary[F[Int]], ArbFString: Arbitrary[F[String]]) extends FoldableCheck[F](name) { def range(start: Long, endInclusive: Long): F[Long] test(s"Reducible[$name].reduceLeftM stack safety") { @@ -95,4 +95,37 @@ abstract class ReducibleCheck[F[_]: Reducible](name: String)(implicit ArbFInt: A fa.nonEmptyIntercalate(a) === (fa.toList.mkString(a)) } } + + + test("Reducible#partitionE retains size") { + forAll { (fi: F[Int], f: Int => Either[String, String]) => + val folded = fi.partitionE(f).fold(identity, identity, _ ++ _.toList) + folded.size.toLong should === (fi.size) + } + } + + test("Reducible#partitionE to one side is identity") { + forAll { (fi: F[Int], f: Int => String) => + 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) + withG should === (Reducible[F].toNonEmptyList((fi.map(f)))) + + val withH = fi.partitionE(h).fold(identity, _ => NonEmptyList.one(""), (l,r) => l) + withH should === (Reducible[F].toNonEmptyList((fi.map(f)))) + } + } + + test("Reducible#partitionE remains sorted") { + forAll { (fi: F[Int], f: Int => Either[String, String]) => + val nel = Reducible[F].toNonEmptyList(fi) + + val sorted = nel.map(f).sorted + val ior = sorted.partitionE(identity) + + ior.left.map(xs => xs.sorted should === (xs)) + ior.right.map(xs => xs.sorted should === (xs)) + } + } }