diff --git a/core/src/main/scala/cats/Bitraverse.scala b/core/src/main/scala/cats/Bitraverse.scala new file mode 100644 index 0000000000..a9a5f9c285 --- /dev/null +++ b/core/src/main/scala/cats/Bitraverse.scala @@ -0,0 +1,45 @@ +package cats + +import cats.functor.Bifunctor + +/** + * A type class abstracting over types that give rise to two independent [[cats.Traverse]]s. + */ +trait Bitraverse[F[_, _]] extends Bifoldable[F] with Bifunctor[F] { self => + /** Traverse each side of the structure with the given functions */ + def bitraverse[G[_]: Applicative, A, B, C, D](fab: F[A, B])(f: A => G[C], g: B => G[D]): G[F[C, D]] + + /** Sequence each side of the structure with the given functions */ + def bisequence[G[_]: Applicative, A, B](fab: F[G[A], G[B]]): G[F[A, B]] = + bitraverse(fab)(identity, identity) + + /** If F and G are both [[cats.Bitraverse]] then so is their composition F[G[_, _], G[_, _]] */ + def compose[G[_, _]](implicit ev: Bitraverse[G]): Bifoldable[Lambda[(A, B) => F[G[A, B], G[A, B]]]] = + new CompositeBitraverse[F, G] { + val F = self + val G = ev + } + + override def bimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): F[C, D] = + bitraverse[Id, A, B, C, D](fab)(f, g) +} + +object Bitraverse { + def apply[F[_, _]](implicit F: Bitraverse[F]): Bitraverse[F] = F +} + +trait CompositeBitraverse[F[_, _], G[_, _]] + extends Bitraverse[Lambda[(A, B) => F[G[A, B], G[A, B]]]] + with CompositeBifoldable[F, G] { + def F: Bitraverse[F] + def G: Bitraverse[G] + + def bitraverse[H[_]: Applicative, A, B, C, D]( + fab: F[G[A, B], G[A, B]])( + f: A => H[C], g: B => H[D] + ): H[F[G[C, D], G[C, D]]] = + F.bitraverse(fab)( + gab => G.bitraverse(gab)(f, g), + gab => G.bitraverse(gab)(f, g) + ) +} diff --git a/core/src/main/scala/cats/data/Xor.scala b/core/src/main/scala/cats/data/Xor.scala index 28e4066b90..b086575a13 100644 --- a/core/src/main/scala/cats/data/Xor.scala +++ b/core/src/main/scala/cats/data/Xor.scala @@ -166,14 +166,20 @@ private[data] sealed abstract class XorInstances extends XorInstances1 { def combine(x: A Xor B, y: A Xor B): A Xor B = x combine y } - implicit def xorBifunctor: Bifunctor[Xor] with Bifoldable[Xor] = - new Bifunctor[Xor] with Bifoldable[Xor]{ - override def bimap[A, B, C, D](fab: A Xor B)(f: A => C, g: B => D): C Xor D = fab.bimap(f, g) + implicit def xorBifunctor: Bitraverse[Xor] = + new Bitraverse[Xor] { + def bitraverse[G[_], A, B, C, D](fab: Xor[A, B])(f: A => G[C], g: B => G[D])(implicit G: Applicative[G]): G[Xor[C, D]] = + fab match { + case Xor.Left(a) => G.map(f(a))(Xor.left) + case Xor.Right(b) => G.map(g(b))(Xor.right) + } + def bifoldLeft[A, B, C](fab: Xor[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C = fab match { case Xor.Left(a) => f(c, a) case Xor.Right(b) => g(c, b) } + def bifoldRight[A, B, C](fab: Xor[A, B], c: Eval[C])(f: (A, Eval[C]) => Eval[C], g: (B, Eval[C]) => Eval[C]): Eval[C] = fab match { case Xor.Left(a) => f(a, c) diff --git a/core/src/main/scala/cats/std/either.scala b/core/src/main/scala/cats/std/either.scala index 8e21c3cdb7..cbabb0d8fa 100644 --- a/core/src/main/scala/cats/std/either.scala +++ b/core/src/main/scala/cats/std/either.scala @@ -2,13 +2,20 @@ package cats package std trait EitherInstances extends EitherInstances1 { - implicit val eitherBifoldable: Bifoldable[Either] = - new Bifoldable[Either] { + implicit val eitherBitraverse: Bitraverse[Either] = + new Bitraverse[Either] { + def bitraverse[G[_], A, B, C, D](fab: Either[A, B])(f: A => G[C], g: B => G[D])(implicit G: Applicative[G]): G[Either[C, D]] = + fab match { + case Left(a) => G.map(f(a))(Left(_)) + case Right(b) => G.map(g(b))(Right(_)) + } + def bifoldLeft[A, B, C](fab: Either[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C = fab match { case Left(a) => f(c, a) case Right(b) => g(c, b) } + def bifoldRight[A, B, C](fab: Either[A, B], c: Eval[C])(f: (A, Eval[C]) => Eval[C], g: (B, Eval[C]) => Eval[C]): Eval[C] = fab match { case Left(a) => f(a, c) diff --git a/core/src/main/scala/cats/std/tuple.scala b/core/src/main/scala/cats/std/tuple.scala index 65f55b419a..cbf9f8d075 100644 --- a/core/src/main/scala/cats/std/tuple.scala +++ b/core/src/main/scala/cats/std/tuple.scala @@ -4,8 +4,11 @@ package std trait TupleInstances extends Tuple2Instances sealed trait Tuple2Instances { - implicit val tuple2Bifoldable: Bifoldable[Tuple2] = - new Bifoldable[Tuple2] { + implicit val tuple2Bitraverse: Bitraverse[Tuple2] = + new Bitraverse[Tuple2] { + def bitraverse[G[_]: Applicative, A, B, C, D](fab: (A, B))(f: A => G[C], g: B => G[D]): G[(C, D)] = + Applicative[G].tuple2(f(fab._1), g(fab._2)) + def bifoldLeft[A, B, C](fab: (A, B), c: C)(f: (C, A) => C, g: (C, B) => C): C = g(f(c, fab._1), fab._2) diff --git a/core/src/main/scala/cats/syntax/all.scala b/core/src/main/scala/cats/syntax/all.scala index fd4786c069..9c943232e3 100644 --- a/core/src/main/scala/cats/syntax/all.scala +++ b/core/src/main/scala/cats/syntax/all.scala @@ -6,6 +6,7 @@ trait AllSyntax with ApplySyntax with BifunctorSyntax with BifoldableSyntax + with BitraverseSyntax with CartesianSyntax with CoflatMapSyntax with ComonadSyntax diff --git a/core/src/main/scala/cats/syntax/bitraverse.scala b/core/src/main/scala/cats/syntax/bitraverse.scala new file mode 100644 index 0000000000..d7903f176c --- /dev/null +++ b/core/src/main/scala/cats/syntax/bitraverse.scala @@ -0,0 +1,15 @@ +package cats +package syntax + +trait BitraverseSyntax { + implicit def bitraverseSyntax[F[_, _]: Bitraverse, A, B](fab: F[A, B]): BitraverseOps[F, A, B] = + new BitraverseOps[F, A, B](fab) +} + +final class BitraverseOps[F[_, _], A, B](fab: F[A, B])(implicit F: Bitraverse[F]) { + def bitraverse[G[_]: Applicative, C, D](f: A => G[C], g: B => G[D]): G[F[C, D]] = + F.bitraverse(fab)(f, g) + + def sequence[G[_], C, D](implicit G: Applicative[G], evLeft: A =:= G[C], evRight: B =:= G[D]): G[F[C, D]] = + F.bisequence(fab.asInstanceOf[F[G[C], G[D]]]) +} diff --git a/core/src/main/scala/cats/syntax/package.scala b/core/src/main/scala/cats/syntax/package.scala index 6391c15220..bd09c018d5 100644 --- a/core/src/main/scala/cats/syntax/package.scala +++ b/core/src/main/scala/cats/syntax/package.scala @@ -6,6 +6,7 @@ package object syntax { object apply extends ApplySyntax object bifunctor extends BifunctorSyntax object bifoldable extends BifoldableSyntax + object bitraverse extends BitraverseSyntax object cartesian extends CartesianSyntax object coflatMap extends CoflatMapSyntax object coproduct extends CoproductSyntax diff --git a/laws/src/main/scala/cats/laws/BitraverseLaws.scala b/laws/src/main/scala/cats/laws/BitraverseLaws.scala new file mode 100644 index 0000000000..b76573ccc3 --- /dev/null +++ b/laws/src/main/scala/cats/laws/BitraverseLaws.scala @@ -0,0 +1,38 @@ +package cats +package laws + +trait BitraverseLaws[F[_, _]] extends BifoldableLaws[F] with BifunctorLaws[F] { + implicit override def F: Bitraverse[F] + + def bitraverseIdentity[A, B](fab: F[A, B]): IsEq[F[A, B]] = + fab <-> F.bitraverse[Id, A, B, A, B](fab)(identity, identity) + + def bitraverseCompose[G[_], A, B, C, D, E, H]( + fab: F[A, B], + f: A => G[C], + g: B => G[D], + h: C => G[E], + i: D => G[H] + )(implicit + G: Applicative[G] + ): IsEq[G[G[F[E, H]]]] = { + val fg = F.bitraverse(fab)(f, g) + val hi = G.map(fg)(f => F.bitraverse(f)(h, i)) + + type GCompose[X] = G[G[X]] + val GCompose = G.compose[G] + + val c = + F.bitraverse[GCompose, A, B, E, H](fab)( + a => G.map(f(a))(h), + b => G.map(g(b))(i) + )(GCompose) + + hi <-> c + } +} + +object BitraverseLaws { + def apply[F[_, _]](implicit ev: Bitraverse[F]): BitraverseLaws[F] = + new BitraverseLaws[F] { def F: Bitraverse[F] = ev } +} diff --git a/laws/src/main/scala/cats/laws/discipline/BitraverseTests.scala b/laws/src/main/scala/cats/laws/discipline/BitraverseTests.scala new file mode 100644 index 0000000000..8b3f9297f3 --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/BitraverseTests.scala @@ -0,0 +1,47 @@ +package cats +package laws +package discipline + +import org.scalacheck.Arbitrary +import org.scalacheck.Prop.forAll + +trait BitraverseTests[F[_, _]] extends BifoldableTests[F] with BifunctorTests[F] { + def laws: BitraverseLaws[F] + + def bitraverse[G[_], A, B, C, D, E, H](implicit + G: Applicative[G], + C: Monoid[C], + ArbFAB: Arbitrary[F[A, B]], + ArbFAD: Arbitrary[F[A, D]], + ArbGC: Arbitrary[G[C]], + ArbGD: Arbitrary[G[D]], + ArbGE: Arbitrary[G[E]], + ArbGH: Arbitrary[G[H]], + ArbA: Arbitrary[A], + ArbB: Arbitrary[B], + ArbC: Arbitrary[C], + ArbE: Arbitrary[E], + ArbH: Arbitrary[H], + EqFAB: Eq[F[A, B]], + EqFAD: Eq[F[A, D]], + EqFAH: Eq[F[A, H]], + EqFCD: Eq[F[C, D]], + EqFCH: Eq[F[C, H]], + EqGGFEH: Eq[G[G[F[E, H]]]], + EqC: Eq[C] + ): RuleSet = + new RuleSet { + val name = "bitraverse" + val parents = Seq(bifoldable[A, B, C], bifunctor[A, B, C, D, E, H]) + val bases = Seq.empty + val props = Seq( + "bitraverse identity" -> forAll(laws.bitraverseIdentity[A, B] _), + "bitraverse composition" -> forAll(laws.bitraverseCompose[G, A, B, C, D, E, H] _) + ) + } +} + +object BitraverseTests { + def apply[F[_, _]: Bitraverse]: BitraverseTests[F] = + new BitraverseTests[F] { def laws: BitraverseLaws[F] = BitraverseLaws[F] } +} diff --git a/tests/src/test/scala/cats/tests/EitherTests.scala b/tests/src/test/scala/cats/tests/EitherTests.scala index 7ae9050340..a104d21cbd 100644 --- a/tests/src/test/scala/cats/tests/EitherTests.scala +++ b/tests/src/test/scala/cats/tests/EitherTests.scala @@ -1,7 +1,7 @@ package cats package tests -import cats.laws.discipline.{BifoldableTests, TraverseTests, MonadTests, SerializableTests, CartesianTests} +import cats.laws.discipline.{BitraverseTests, TraverseTests, MonadTests, SerializableTests, CartesianTests} import cats.laws.discipline.eq._ import algebra.laws.OrderLaws @@ -18,8 +18,8 @@ class EitherTests extends CatsSuite { checkAll("Either[Int, Int] with Option", TraverseTests[Either[Int, ?]].traverse[Int, Int, Int, Int, Option, Option]) checkAll("Traverse[Either[Int, ?]", SerializableTests.serializable(Traverse[Either[Int, ?]])) - checkAll("Either[?, ?]", BifoldableTests[Either].bifoldable[Int, Int, Int]) - checkAll("Bifoldable[Either]", SerializableTests.serializable(Bifoldable[Either])) + checkAll("Either[?, ?]", BitraverseTests[Either].bitraverse[Option, Int, Int, Int, String, String, String]) + checkAll("Bitraverse[Either]", SerializableTests.serializable(Bitraverse[Either])) val partialOrder = eitherPartialOrder[Int, String] val order = implicitly[Order[Either[Int, String]]] diff --git a/tests/src/test/scala/cats/tests/TupleTests.scala b/tests/src/test/scala/cats/tests/TupleTests.scala index 066c52f00e..419fabc698 100644 --- a/tests/src/test/scala/cats/tests/TupleTests.scala +++ b/tests/src/test/scala/cats/tests/TupleTests.scala @@ -1,9 +1,10 @@ package cats package tests -import cats.laws.discipline.{BifoldableTests, SerializableTests} +import cats.laws.discipline.{BitraverseTests, SerializableTests} +import cats.laws.discipline.eq.tuple2Eq class TupleTests extends CatsSuite { - checkAll("Tuple2", BifoldableTests[Tuple2].bifoldable[Int, Int, Int]) - checkAll("Bifoldable[Tuple2]", SerializableTests.serializable(Bifoldable[Tuple2])) + checkAll("Tuple2", BitraverseTests[Tuple2].bitraverse[Option, Int, Int, Int, String, String, String]) + checkAll("Bitraverse[Tuple2]", SerializableTests.serializable(Bitraverse[Tuple2])) } diff --git a/tests/src/test/scala/cats/tests/XorTests.scala b/tests/src/test/scala/cats/tests/XorTests.scala index ec6d0256a0..9d3325d9db 100644 --- a/tests/src/test/scala/cats/tests/XorTests.scala +++ b/tests/src/test/scala/cats/tests/XorTests.scala @@ -3,9 +3,8 @@ package tests import cats.data.{NonEmptyList, Xor, XorT} import cats.data.Xor._ -import cats.functor.Bifunctor import cats.laws.discipline.arbitrary._ -import cats.laws.discipline.{BifunctorTests, BifoldableTests, TraverseTests, MonadErrorTests, SerializableTests, CartesianTests} +import cats.laws.discipline.{BitraverseTests, TraverseTests, MonadErrorTests, SerializableTests, CartesianTests} import cats.laws.discipline.eq.tuple3Eq import algebra.laws.{GroupLaws, OrderLaws} import org.scalacheck.{Arbitrary, Gen} @@ -55,11 +54,8 @@ class XorTests extends CatsSuite { } yield xor } - checkAll("? Xor ?", BifunctorTests[Xor].bifunctor[Int, Int, Int, String, String, String]) - checkAll("Bifunctor[Xor]", SerializableTests.serializable(Bifunctor[Xor])) - - checkAll("? Xor ?", BifoldableTests[Xor].bifoldable[Int, Int, Int]) - checkAll("Bifoldable[Xor]", SerializableTests.serializable(Bifoldable[Xor])) + checkAll("? Xor ?", BitraverseTests[Xor].bitraverse[Option, Int, Int, Int, String, String, String]) + checkAll("Bitraverse[Xor]", SerializableTests.serializable(Bitraverse[Xor])) test("catchOnly catches matching exceptions") { assert(Xor.catchOnly[NumberFormatException]{ "foo".toInt }.isInstanceOf[Xor.Left[NumberFormatException]])