diff --git a/core/src/main/scala/cats/Parallel.scala b/core/src/main/scala/cats/Parallel.scala index 065ffbe0e2..d5060ff7a2 100644 --- a/core/src/main/scala/cats/Parallel.scala +++ b/core/src/main/scala/cats/Parallel.scala @@ -312,6 +312,18 @@ object Parallel extends ParallelArityFunctions2 { P.sequential(ftab) } + /** + * Like `Foldable[A].foldMapA`, but uses the applicative instance + * corresponding to the Parallel instance instead. + */ + def parFoldMapA[T[_], M[_], A, B]( + ta: T[A] + )(f: A => M[B])(implicit T: Foldable[T], P: Parallel[M], B: Monoid[B]): M[B] = { + val fb: P.F[B] = + Foldable[T].foldMapA(ta)(a => P.parallel(f(a)))(P.applicative, B) + P.sequential(fb) + } + /** * Like `Applicative[F].ap`, but uses the applicative instance * corresponding to the Parallel instance instead. diff --git a/core/src/main/scala/cats/syntax/all.scala b/core/src/main/scala/cats/syntax/all.scala index e93083fe37..c19ea72e45 100644 --- a/core/src/main/scala/cats/syntax/all.scala +++ b/core/src/main/scala/cats/syntax/all.scala @@ -59,6 +59,7 @@ trait AllSyntax with ValidatedSyntax with VectorSyntax with WriterSyntax + with ParallelFoldMapASyntax trait AllSyntaxBinCompat0 extends UnorderedTraverseSyntax with ApplicativeErrorExtension with TrySyntax diff --git a/core/src/main/scala/cats/syntax/package.scala b/core/src/main/scala/cats/syntax/package.scala index 595573dce4..8db5df771d 100644 --- a/core/src/main/scala/cats/syntax/package.scala +++ b/core/src/main/scala/cats/syntax/package.scala @@ -47,6 +47,7 @@ package object syntax { with ParallelApplySyntax with ParallelBitraverseSyntax with ParallelUnorderedTraverseSyntax + with ParallelFoldMapASyntax object partialOrder extends PartialOrderSyntax object profunctor extends ProfunctorSyntax object reducible extends ReducibleSyntax with ReducibleSyntaxBinCompat0 diff --git a/core/src/main/scala/cats/syntax/parallel.scala b/core/src/main/scala/cats/syntax/parallel.scala index f21e4982bf..e7406fe063 100644 --- a/core/src/main/scala/cats/syntax/parallel.scala +++ b/core/src/main/scala/cats/syntax/parallel.scala @@ -1,6 +1,16 @@ package cats.syntax -import cats.{Bitraverse, CommutativeApplicative, FlatMap, Foldable, Monad, Parallel, Traverse, UnorderedTraverse} +import cats.{ + Bitraverse, + CommutativeApplicative, + FlatMap, + Foldable, + Monad, + Monoid, + Parallel, + Traverse, + UnorderedTraverse +} trait ParallelSyntax extends TupleParallelSyntax { @@ -79,6 +89,11 @@ trait ParallelUnorderedTraverseSyntax { } +trait ParallelFoldMapASyntax { + implicit final def catsSyntaxParallelFoldMapA[T[_], A](ta: T[A]): ParallelFoldMapAOps[T, A] = + new ParallelFoldMapAOps[T, A](ta) +} + final class ParallelTraversableOps[T[_], A](private val ta: T[A]) extends AnyVal { def parTraverse[M[_]: Monad, B](f: A => M[B])(implicit T: Traverse[T], P: Parallel[M]): M[T[B]] = Parallel.parTraverse(ta)(f) @@ -176,3 +191,8 @@ final class ParallelLeftSequenceOps[T[_, _], M[_], A, B](private val tmab: T[M[A def parLeftSequence(implicit T: Bitraverse[T], P: Parallel[M]): M[T[A, B]] = Parallel.parLeftSequence(tmab) } + +final class ParallelFoldMapAOps[T[_], A](private val ma: T[A]) extends AnyVal { + def parFoldMapA[M[_], B](f: A => M[B])(implicit T: Foldable[T], P: Parallel[M], B: Monoid[B]): M[B] = + Parallel.parFoldMapA(ma)(f) +} diff --git a/tests/src/test/scala/cats/tests/ParallelSuite.scala b/tests/src/test/scala/cats/tests/ParallelSuite.scala index 8d052df629..8069a776a0 100644 --- a/tests/src/test/scala/cats/tests/ParallelSuite.scala +++ b/tests/src/test/scala/cats/tests/ParallelSuite.scala @@ -230,6 +230,14 @@ class ParallelSuite extends CatsSuite with ApplicativeErrorForEitherTest with Sc } } + test("ParFoldMapA should be equivalent to parTraverse map combineAll (where it exists)") { + forAll { (es: List[Int], f: Int => Either[String, String]) => + Parallel.parFoldMapA(es)(f) should ===( + Parallel.parTraverse(es)(f).map(_.combineAll) + ) + } + } + test("parAp accumulates errors in order") { val right: Either[String, Int => Int] = Left("Hello") Parallel.parAp(right)("World".asLeft) should ===(Left("HelloWorld")) diff --git a/tests/src/test/scala/cats/tests/SyntaxSuite.scala b/tests/src/test/scala/cats/tests/SyntaxSuite.scala index f2d20fa70a..04fb3f2f10 100644 --- a/tests/src/test/scala/cats/tests/SyntaxSuite.scala +++ b/tests/src/test/scala/cats/tests/SyntaxSuite.scala @@ -252,6 +252,12 @@ object SyntaxSuite val mtab2 = tmab.parLeftSequence } + def testParallelFoldable[T[_]: Foldable, M[_]: Parallel, A, B: Monoid]: Unit = { + val ta = mock[T[A]] + val f = mock[A => M[B]] + val mb = ta.parFoldMapA(f) + } + def testReducible[F[_]: Reducible, G[_]: Apply: SemigroupK, A: Semigroup, B, Z]: Unit = { val fa = mock[F[A]] val f1 = mock[(A, A) => A]