diff --git a/core/src/main/scala/cats/Parallel.scala b/core/src/main/scala/cats/Parallel.scala index 7c36aa599e..2f905da39d 100644 --- a/core/src/main/scala/cats/Parallel.scala +++ b/core/src/main/scala/cats/Parallel.scala @@ -235,6 +235,52 @@ object Parallel extends ParallelArityFunctions2 { P.sequential(gtb) } + /** + * Like `Bitraverse[A].bitraverse`, but uses the applicative instance + * corresponding to the Parallel instance instead. + */ + def parBitraverse[T[_, _]: Bitraverse, M[_], F[_], A, B, C, D]( + tab: T[A, B] + )(f: A => M[C], g: B => M[D])(implicit P: Parallel[M, F]): M[T[C, D]] = { + val ftcd: F[T[C, D]] = + Bitraverse[T].bitraverse(tab)(f.andThen(P.parallel.apply), g.andThen(P.parallel.apply))(P.applicative) + P.sequential(ftcd) + } + + /** + * Like `Bitraverse[A].bisequence`, but uses the applicative instance + * corresponding to the Parallel instance instead. + */ + def parBisequence[T[_, _]: Bitraverse, M[_], F[_], A, B]( + tmamb: T[M[A], M[B]] + )(implicit P: Parallel[M, F]): M[T[A, B]] = { + val ftab: F[T[A, B]] = Bitraverse[T].bitraverse(tmamb)(P.parallel.apply, P.parallel.apply)(P.applicative) + P.sequential(ftab) + } + + /** + * Like `Bitraverse[A].leftTraverse`, but uses the applicative instance + * corresponding to the Parallel instance instead. + */ + def parLeftTraverse[T[_, _]: Bitraverse, M[_], F[_], A, B, C]( + tab: T[A, B] + )(f: A => M[C])(implicit P: Parallel[M, F]): M[T[C, B]] = { + val ftcb: F[T[C, B]] = + Bitraverse[T].bitraverse(tab)(f.andThen(P.parallel.apply), P.applicative.pure)(P.applicative) + P.sequential(ftcb) + } + + /** + * Like `Bitraverse[A].leftSequence`, but uses the applicative instance + * corresponding to the Parallel instance instead. + */ + def parLeftSequence[T[_, _]: Bitraverse, M[_], F[_], A, B]( + tmab: T[M[A], B] + )(implicit P: Parallel[M, F]): M[T[A, B]] = { + val ftab: F[T[A, B]] = Bitraverse[T].bitraverse(tmab)(P.parallel.apply, P.applicative.pure)(P.applicative) + P.sequential(ftab) + } + /** * 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 662ab20c82..a12639f1b9 100644 --- a/core/src/main/scala/cats/syntax/all.scala +++ b/core/src/main/scala/cats/syntax/all.scala @@ -8,6 +8,7 @@ abstract class AllSyntaxBinCompat with AllSyntaxBinCompat2 with AllSyntaxBinCompat3 with AllSyntaxBinCompat4 + with AllSyntaxBinCompat5 trait AllSyntax extends AlternativeSyntax @@ -87,3 +88,5 @@ trait AllSyntaxBinCompat4 with ReducibleSyntaxBinCompat0 with FoldableSyntaxBinCompat1 with BitraverseSyntaxBinCompat0 + +trait AllSyntaxBinCompat5 extends ParallelBitraverseSyntax diff --git a/core/src/main/scala/cats/syntax/package.scala b/core/src/main/scala/cats/syntax/package.scala index 8ae7e72db9..f7f4170aad 100644 --- a/core/src/main/scala/cats/syntax/package.scala +++ b/core/src/main/scala/cats/syntax/package.scala @@ -39,7 +39,12 @@ package object syntax { object nested extends NestedSyntax object option extends OptionSyntax object order extends OrderSyntax - object parallel extends ParallelSyntax with ParallelTraverseSyntax with ParallelFlatSyntax with ParallelApplySyntax + object parallel + extends ParallelSyntax + with ParallelTraverseSyntax + with ParallelFlatSyntax + with ParallelApplySyntax + with ParallelBitraverseSyntax 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 a0ff31fc35..79bd2ed696 100644 --- a/core/src/main/scala/cats/syntax/parallel.scala +++ b/core/src/main/scala/cats/syntax/parallel.scala @@ -1,6 +1,6 @@ package cats.syntax -import cats.{FlatMap, Foldable, Monad, Parallel, Traverse} +import cats.{Bitraverse, FlatMap, Foldable, Monad, Parallel, Traverse} trait ParallelSyntax extends TupleParallelSyntax { @@ -39,6 +39,28 @@ trait ParallelTraverseSyntax { new ParallelSequence_Ops[T, M, A](tma) } +trait ParallelBitraverseSyntax { + implicit final def catsSyntaxParallelBitraverse[T[_, _]: Bitraverse, A, B]( + tab: T[A, B] + ): ParallelBitraverseOps[T, A, B] = + new ParallelBitraverseOps[T, A, B](tab) + + implicit final def catsSyntaxParallelBisequence[T[_, _]: Bitraverse, M[_], A, B]( + tmamb: T[M[A], M[B]] + ): ParallelBisequenceOps[T, M, A, B] = + new ParallelBisequenceOps[T, M, A, B](tmamb) + + implicit final def catsSyntaxParallelLeftTraverse[T[_, _]: Bitraverse, A, B]( + tab: T[A, B] + ): ParallelLeftTraverseOps[T, A, B] = + new ParallelLeftTraverseOps[T, A, B](tab) + + implicit final def catsSyntaxParallelLeftSequence[T[_, _]: Bitraverse, M[_], A, B]( + tmab: T[M[A], B] + ): ParallelLeftSequenceOps[T, M, A, B] = + new ParallelLeftSequenceOps[T, M, A, B](tmab) +} + final class ParallelTraversableOps[T[_], A](private val ta: T[A]) extends AnyVal { def parTraverse[M[_]: Monad, F[_], B](f: A => M[B])(implicit T: Traverse[T], P: Parallel[M, F]): M[T[B]] = Parallel.parTraverse(ta)(f) @@ -86,3 +108,24 @@ final class ParallelApplyOps[M[_], A, B](private val mab: M[A => B]) extends Any def <&>[F[_]](ma: M[A])(implicit P: Parallel[M, F]): M[B] = Parallel.parAp(mab)(ma) } + +final class ParallelBitraverseOps[T[_, _], A, B](private val tab: T[A, B]) extends AnyVal { + def parBitraverse[M[_], F[_], C, D](f: A => M[C], g: B => M[D])(implicit T: Bitraverse[T], + P: Parallel[M, F]): M[T[C, D]] = + Parallel.parBitraverse(tab)(f, g) +} + +final class ParallelBisequenceOps[T[_, _], M[_], A, B](private val tmamb: T[M[A], M[B]]) extends AnyVal { + def parBisequence[F[_]](implicit T: Bitraverse[T], P: Parallel[M, F]): M[T[A, B]] = + Parallel.parBisequence(tmamb) +} + +final class ParallelLeftTraverseOps[T[_, _], A, B](private val tab: T[A, B]) extends AnyVal { + def parLeftTraverse[M[_], F[_], C](f: A => M[C])(implicit T: Bitraverse[T], P: Parallel[M, F]): M[T[C, B]] = + Parallel.parLeftTraverse(tab)(f) +} + +final class ParallelLeftSequenceOps[T[_, _], M[_], A, B](private val tmab: T[M[A], B]) extends AnyVal { + def parLeftSequence[F[_]](implicit T: Bitraverse[T], P: Parallel[M, F]): M[T[A, B]] = + Parallel.parLeftSequence(tmab) +} diff --git a/testkit/src/main/scala/cats/tests/CatsSuite.scala b/testkit/src/main/scala/cats/tests/CatsSuite.scala index 7cfe0838dc..9a548cb279 100644 --- a/testkit/src/main/scala/cats/tests/CatsSuite.scala +++ b/testkit/src/main/scala/cats/tests/CatsSuite.scala @@ -47,6 +47,7 @@ trait CatsSuite with AllSyntaxBinCompat2 with AllSyntaxBinCompat3 with AllSyntaxBinCompat4 + with AllSyntaxBinCompat5 with StrictCatsEquality { self: FunSuiteLike => implicit override val generatorDrivenConfig: PropertyCheckConfiguration = diff --git a/tests/src/test/scala/cats/tests/ParallelSuite.scala b/tests/src/test/scala/cats/tests/ParallelSuite.scala index f99728ac68..cae6136a71 100644 --- a/tests/src/test/scala/cats/tests/ParallelSuite.scala +++ b/tests/src/test/scala/cats/tests/ParallelSuite.scala @@ -81,6 +81,113 @@ class ParallelSuite extends CatsSuite with ApplicativeErrorForEitherTest { } } + type ListTuple2[A, B] = List[(A, B)] + implicit val catsBitraverseForListTuple2 = new Bitraverse[ListTuple2] { + def bifoldLeft[A, B, C](fab: ListTuple2[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C = + fab.foldLeft(c) { case (c, (a, b)) => g(f(c, a), b) } + def bifoldRight[A, B, C](fab: ListTuple2[A, B], lc: Eval[C])(f: (A, Eval[C]) => Eval[C], + g: (B, Eval[C]) => Eval[C]): Eval[C] = { + def loop(abs: ListTuple2[A, B]): Eval[C] = + abs match { + case Nil => lc + case (a, b) :: t => f(a, g(b, Eval.defer(loop(t)))) + } + Eval.defer(loop(fab)) + } + def bitraverse[G[_], A, B, C, D]( + fab: ListTuple2[A, B] + )(f: A => G[C], g: B => G[D])(implicit G: Applicative[G]): G[ListTuple2[C, D]] = { + def loop(abs: ListTuple2[A, B]): Eval[G[ListTuple2[C, D]]] = + abs match { + case Nil => Now(G.pure(List.empty)) + case (a, b) :: t => G.map2Eval(G.product(f(a), g(b)), Eval.defer(loop(t)))(_ :: _) + } + loop(fab).value + } + } + + test("ParBisequence Either should accumulate errors") { + forAll { es: ListTuple2[Either[String, Int], Either[String, Int]] => + val lefts = es + .flatMap { + case (a, b) => List(a, b) + } + .collect { + case Left(e) => e + } + .foldMap(identity) + + es.parBisequence.fold(identity, i => Monoid[String].empty) should ===(lefts) + } + } + + test("ParBisequence Ior should accumulate errors") { + forAll { es: ListTuple2[Ior[String, Int], Ior[String, Int]] => + val lefts = es + .flatMap { + case (a, b) => List(a, b) + } + .map(_.left) + .collect { + case Some(e) => e + } + .foldMap(identity) + + es.parBisequence.left.getOrElse(Monoid[String].empty) should ===(lefts) + } + } + + test("ParBisequence Ior should bisequence values") { + forAll { es: ListTuple2[Ior[String, Int], Ior[String, Int]] => + es.parBisequence.right should ===(es.bimap(_.toOption, _.toOption).bisequence) + } + } + + test("ParBitraverse identity should be equivalent to parBisequence") { + forAll { es: (Either[String, Int], Either[String, Long]) => + es.parBitraverse(identity, identity) should ===(es.parBisequence) + } + } + + test("ParLeftSequence Either should accumulate errors") { + forAll { es: ListTuple2[Either[String, Int], Int] => + val lefts = es + .collect { + case (Left(e), _) => e + } + .foldMap(identity) + + es.parLeftSequence.fold(identity, i => Monoid[String].empty) should ===(lefts) + } + } + + test("ParLeftSequence Ior should accumulate errors") { + forAll { es: ListTuple2[Ior[String, Int], Int] => + val lefts = es + .map { + case (a, b) => a.left + } + .collect { + case Some(e) => e + } + .foldMap(identity) + + es.parLeftSequence.left.getOrElse(Monoid[String].empty) should ===(lefts) + } + } + + test("ParLeftSequence Ior should leftSequence values") { + forAll { es: ListTuple2[Ior[String, Int], Int] => + es.parLeftSequence.right should ===(es.bimap(_.toOption, identity).leftSequence) + } + } + + test("ParLeftTraverse identity should be equivalent to parLeftSequence") { + forAll { es: (Either[String, Int], Either[String, Long]) => + es.parLeftTraverse(identity) should ===(es.parLeftSequence) + } + } + test("ParFlatTraverse should be equivalent to parTraverse map flatten") { forAll { es: List[Either[String, Int]] => val f: Int => List[Int] = i => List(i, i + 1) diff --git a/tests/src/test/scala/cats/tests/SyntaxSuite.scala b/tests/src/test/scala/cats/tests/SyntaxSuite.scala index 74388d57d5..0cff4ecc64 100644 --- a/tests/src/test/scala/cats/tests/SyntaxSuite.scala +++ b/tests/src/test/scala/cats/tests/SyntaxSuite.scala @@ -208,6 +208,20 @@ object SyntaxSuite (fa, fb, fc).parMapN(f) } + def testParallelBi[M[_], F[_], T[_, _]: Bitraverse, A, B, C, D](implicit P: Parallel[M, F]): Unit = { + val tab = mock[T[A, B]] + val f = mock[A => M[C]] + val g = mock[B => M[D]] + val mtcd = tab.parBitraverse(f, g) + val mtcb = tab.parLeftTraverse(f) + + val tmamb = mock[T[M[A], M[B]]] + val mtab1 = tmamb.parBisequence + + val tmab = mock[T[M[A], B]] + val mtab2 = tmab.parLeftSequence + } + def testReducible[F[_]: Reducible, G[_]: Apply: SemigroupK, A: Semigroup, B, Z]: Unit = { val fa = mock[F[A]] val f1 = mock[(A, A) => A]