From 70c9625255dee4bf3584931ba9a1ceb01bb293e7 Mon Sep 17 00:00:00 2001 From: Maxim Davydov Date: Tue, 10 Dec 2019 19:10:12 +0300 Subject: [PATCH] Issue 3141 (#3150) * #3141 [WIP] Implemented .foldA * #3141 [WIP] Implemented .reduceA * #3141 [WIP] Implemented .reduceMapA * #3141 Refactored * #3141 Added tests * #3141 Fixed 2.13 * #3141 Added link to the issue at simulacrum * #3141 Fixed docs * #3141 Formatted * #3141 Refactored * #3141 Changed .reduceMap signature * #3141 Removed noop on .reduceMapA * #3141 Fixed docs * #3141 Removed changes from reduceMapK --- core/src/main/scala/cats/Foldable.scala | 20 +++++++++++++ core/src/main/scala/cats/Reducible.scala | 16 +++++++++++ .../src/main/scala/cats/syntax/foldable.scala | 3 ++ .../main/scala/cats/syntax/reducible.scala | 3 ++ .../cats/tests/NonEmptyStreamSuite.scala | 3 ++ .../cats/tests/NonEmptyLazyListSuite.scala | 3 ++ .../test/scala/cats/tests/FoldableSuite.scala | 14 ++++++++++ .../scala/cats/tests/NonEmptyChainSuite.scala | 3 ++ .../scala/cats/tests/NonEmptyListSuite.scala | 2 ++ .../cats/tests/NonEmptyVectorSuite.scala | 3 ++ .../scala/cats/tests/ReducibleSuite.scala | 28 +++++++++++++++++++ 11 files changed, 98 insertions(+) diff --git a/core/src/main/scala/cats/Foldable.scala b/core/src/main/scala/cats/Foldable.scala index 813eb76b3c..21ce2f8669 100644 --- a/core/src/main/scala/cats/Foldable.scala +++ b/core/src/main/scala/cats/Foldable.scala @@ -394,6 +394,26 @@ import Foldable.sentinel } } + /** + * Fold implemented using the given `Applicative[G]` and `Monoid[A]` instance. + * + * This method is identical to fold, except that we use `Applicative[G]` and `Monoid[A]` + * to combine a's inside an applicative G. + * + * For example: + * + * {{{ + * scala> import cats.implicits._ + * scala> val F = Foldable[List] + * scala> F.foldA(List(Either.right[String, Int](1), Either.right[String, Int](2))) + * res0: Either[String, Int] = Right(3) + * }}} + * + * `noop` usage description [[https://github.com/typelevel/simulacrum/issues/162 here]] + */ + @noop def foldA[G[_], A](fga: F[G[A]])(implicit G: Applicative[G], A: Monoid[A]): G[A] = + fold(fga)(Applicative.monoid) + /** * Fold implemented by mapping `A` values into `B` in a context `G` and then * combining them using the `MonoidK[G]` instance. diff --git a/core/src/main/scala/cats/Reducible.scala b/core/src/main/scala/cats/Reducible.scala index 10865dee82..a3d18e8417 100644 --- a/core/src/main/scala/cats/Reducible.scala +++ b/core/src/main/scala/cats/Reducible.scala @@ -82,6 +82,22 @@ import simulacrum.{noop, typeclass} def reduceLeftM[G[_], A, B](fa: F[A])(f: A => G[B])(g: (B, A) => G[B])(implicit G: FlatMap[G]): G[B] = reduceLeftTo(fa)(f)((gb, a) => G.flatMap(gb)(g(_, a))) + /** + * Reduce a `F[G[A]]` value using `Applicative[G]` and `Semigroup[A]`, a universal + * semigroup for `G[_]`. + * + * `noop` usage description [[https://github.com/typelevel/simulacrum/issues/162 here]] + */ + @noop def reduceA[G[_], A](fga: F[G[A]])(implicit G: Apply[G], A: Semigroup[A]): G[A] = + reduce(fga)(Apply.semigroup) + + /** + * Apply `f` to each `a` of `fa` and combine the result into Apply[G] using the + * given `Semigroup[B]`. + */ + def reduceMapA[G[_], A, B](fa: F[A])(f: A => G[B])(implicit G: Apply[G], B: Semigroup[B]): G[B] = + reduceLeftTo(fa)(f)((gb, a) => G.map2(gb, f(a))(B.combine)) + /** * Monadic reducing by mapping the `A` values to `G[B]`. combining * the `B` values using the given `Semigroup[B]` instance. diff --git a/core/src/main/scala/cats/syntax/foldable.scala b/core/src/main/scala/cats/syntax/foldable.scala index 6f3909f1d8..729f3f100e 100644 --- a/core/src/main/scala/cats/syntax/foldable.scala +++ b/core/src/main/scala/cats/syntax/foldable.scala @@ -46,6 +46,9 @@ final class FoldableOps[F[_], A](private val fa: F[A]) extends AnyVal { def foldr[B](b: Eval[B])(f: (A, Eval[B]) => Eval[B])(implicit F: Foldable[F]): Eval[B] = F.foldRight(fa, b)(f) + def foldA[G[_], B](implicit F: Foldable[F], ev: A <:< G[B], G: Applicative[G], B: Monoid[B]): G[B] = + F.foldA[G, B](fa.asInstanceOf[F[G[B]]]) + /** * test if `F[A]` contains an `A`, named contains_ to avoid conflict with existing contains which uses universal equality * diff --git a/core/src/main/scala/cats/syntax/reducible.scala b/core/src/main/scala/cats/syntax/reducible.scala index 76e1ff1faf..0b542c8742 100644 --- a/core/src/main/scala/cats/syntax/reducible.scala +++ b/core/src/main/scala/cats/syntax/reducible.scala @@ -30,4 +30,7 @@ final class ReducibleOps0[F[_], A](private val fa: F[A]) extends AnyVal { * }}} * */ def reduceMapK[G[_], B](f: A => G[B])(implicit F: Reducible[F], G: SemigroupK[G]): G[B] = F.reduceMapK[G, A, B](fa)(f) + + def reduceA[G[_], B](implicit F: Reducible[F], ev: A <:< G[B], G: Apply[G], B: Semigroup[B]): G[B] = + F.reduceA[G, B](fa.asInstanceOf[F[G[B]]]) } diff --git a/tests/src/test/scala-2.12/cats/tests/NonEmptyStreamSuite.scala b/tests/src/test/scala-2.12/cats/tests/NonEmptyStreamSuite.scala index 33774472eb..4e4e779aba 100644 --- a/tests/src/test/scala-2.12/cats/tests/NonEmptyStreamSuite.scala +++ b/tests/src/test/scala-2.12/cats/tests/NonEmptyStreamSuite.scala @@ -166,4 +166,7 @@ class ReducibleNonEmptyStreamSuite extends ReducibleSuite[NonEmptyStream]("NonEm val tailStart: Long = start + 1L NonEmptyStream(start, tailStart.to(endInclusive).toStream) } + + def rangeE[L, R](el: Either[L, R], els: Either[L, R]*): NonEmptyStream[Either[L, R]] = + NonEmptyStream(el, els: _*) } diff --git a/tests/src/test/scala-2.13+/cats/tests/NonEmptyLazyListSuite.scala b/tests/src/test/scala-2.13+/cats/tests/NonEmptyLazyListSuite.scala index 2468af0aa3..1887844853 100644 --- a/tests/src/test/scala-2.13+/cats/tests/NonEmptyLazyListSuite.scala +++ b/tests/src/test/scala-2.13+/cats/tests/NonEmptyLazyListSuite.scala @@ -137,4 +137,7 @@ class ReducibleNonEmptyLazyListSuite extends ReducibleSuite[NonEmptyLazyList]("N def range(start: Long, endInclusive: Long): NonEmptyLazyList[Long] = NonEmptyLazyList(start, (start + 1L).to(endInclusive): _*) + + def rangeE[L, R](el: Either[L, R], els: Either[L, R]*): NonEmptyLazyList[Either[L, R]] = + NonEmptyLazyList(el, els: _*) } diff --git a/tests/src/test/scala/cats/tests/FoldableSuite.scala b/tests/src/test/scala/cats/tests/FoldableSuite.scala index a1a5c74ed3..6df432fc5b 100644 --- a/tests/src/test/scala/cats/tests/FoldableSuite.scala +++ b/tests/src/test/scala/cats/tests/FoldableSuite.scala @@ -456,6 +456,20 @@ class FoldableSuiteAdditional extends CatsSuite with ScalaVersionSpecificFoldabl Foldable[Stream].foldRight(fa, lb)(f) } + test(".foldA successful case") { + implicit val F = foldableStreamWithDefaultImpl + val ns = Stream.apply[Either[String, Int]](1.asRight, 2.asRight, 7.asRight) + + assert(F.foldA(ns) == 10.asRight[String]) + } + + test(".foldA failed case") { + implicit val F = foldableStreamWithDefaultImpl + val ns = Stream.apply[Either[String, Int]](1.asRight, "boom!!!".asLeft, 7.asRight) + + assert(ns.foldA == "boom!!!".asLeft[Int]) + } + test(".foldLeftM short-circuiting") { implicit val F = foldableStreamWithDefaultImpl val ns = Stream.continually(1) diff --git a/tests/src/test/scala/cats/tests/NonEmptyChainSuite.scala b/tests/src/test/scala/cats/tests/NonEmptyChainSuite.scala index 4bf6412f45..5cdf19dbba 100644 --- a/tests/src/test/scala/cats/tests/NonEmptyChainSuite.scala +++ b/tests/src/test/scala/cats/tests/NonEmptyChainSuite.scala @@ -161,4 +161,7 @@ class ReducibleNonEmptyChainSuite extends ReducibleSuite[NonEmptyChain]("NonEmpt def range(start: Long, endInclusive: Long): NonEmptyChain[Long] = NonEmptyChain(start, (start + 1L).to(endInclusive): _*) + + def rangeE[L, R](el: Either[L, R], els: Either[L, R]*): NonEmptyChain[Either[L, R]] = + NonEmptyChain(el, els: _*) } diff --git a/tests/src/test/scala/cats/tests/NonEmptyListSuite.scala b/tests/src/test/scala/cats/tests/NonEmptyListSuite.scala index c2fc4b4da1..c2ef65ae76 100644 --- a/tests/src/test/scala/cats/tests/NonEmptyListSuite.scala +++ b/tests/src/test/scala/cats/tests/NonEmptyListSuite.scala @@ -370,4 +370,6 @@ class ReducibleNonEmptyListSuite extends ReducibleSuite[NonEmptyList]("NonEmptyL NonEmptyList(start, (tailStart).to(endInclusive).toList) } + def rangeE[L, R](el: Either[L, R], els: Either[L, R]*): NonEmptyList[Either[L, R]] = + NonEmptyList(el, List(els: _*)) } diff --git a/tests/src/test/scala/cats/tests/NonEmptyVectorSuite.scala b/tests/src/test/scala/cats/tests/NonEmptyVectorSuite.scala index f0cdc21f87..7577645920 100644 --- a/tests/src/test/scala/cats/tests/NonEmptyVectorSuite.scala +++ b/tests/src/test/scala/cats/tests/NonEmptyVectorSuite.scala @@ -403,4 +403,7 @@ class ReducibleNonEmptyVectorSuite extends ReducibleSuite[NonEmptyVector]("NonEm val tailStart: Long = start + 1L NonEmptyVector(start, (tailStart).to(endInclusive).toVector) } + + def rangeE[L, R](el: Either[L, R], els: Either[L, R]*): NonEmptyVector[Either[L, R]] = + NonEmptyVector(el, Vector(els: _*)) } diff --git a/tests/src/test/scala/cats/tests/ReducibleSuite.scala b/tests/src/test/scala/cats/tests/ReducibleSuite.scala index 77ae2bd201..88ab34a2ec 100644 --- a/tests/src/test/scala/cats/tests/ReducibleSuite.scala +++ b/tests/src/test/scala/cats/tests/ReducibleSuite.scala @@ -106,6 +106,7 @@ abstract class ReducibleSuite[F[_]: Reducible](name: String)(implicit ArbFInt: A extends FoldableSuite[F](name) { def range(start: Long, endInclusive: Long): F[Long] + def rangeE[L, R](el: Either[L, R], els: Either[L, R]*): F[Either[L, R]] test(s"Reducible[$name].reduceLeftM stack safety") { def nonzero(acc: Long, x: Long): Option[Long] = @@ -117,6 +118,33 @@ abstract class ReducibleSuite[F[_]: Reducible](name: String)(implicit ArbFInt: A actual should ===(Some(expected)) } + test(s"Reducible[$name].reduceA successful case") { + val expected = 6 + val actual = rangeE(1.asRight[String], 2.asRight[String], 3.asRight[String]).reduceA + actual should ===(expected.asRight[String]) + } + + test(s"Reducible[$name].reduceA failure case") { + val expected = "boom!!!" + val actual = rangeE(1.asRight, "boom!!!".asLeft, 3.asRight).reduceA + actual should ===(expected.asLeft[Int]) + } + + test(s"Reducible[$name].reduceMapA successful case") { + val expected = "123" + val actual = range(1, 3).reduceMapA(_.toString.some) + + actual should ===(expected.some) + } + + test(s"Reducible[$name].reduceMapA failure case") { + def intToString(i: Long): Either[String, Int] = if (i == 2) i.toInt.asRight else "boom!!!".asLeft + + val expected = "boom!!!" + val actual = range(1, 3).reduceMapA(intToString) + actual should ===(expected.asLeft[Int]) + } + test(s"Reducible[$name].toNonEmptyList/toList consistency") { forAll { (fa: F[Int]) => fa.toList.toNel should ===(Some(fa.toNonEmptyList))