diff --git a/core/src/main/scala/cats/functor/Bifunctor.scala b/core/src/main/scala/cats/functor/Bifunctor.scala index af9a082945..0043981603 100644 --- a/core/src/main/scala/cats/functor/Bifunctor.scala +++ b/core/src/main/scala/cats/functor/Bifunctor.scala @@ -23,8 +23,26 @@ trait Bifunctor[F[_, _]] extends Any with Serializable { self => * apply a function ro the "right" functor */ def rightMap[A,B,C](fab: F[A, B])(f: B => C): F[A,C] = bimap(fab)(identity, f) + + /** The composition of two Bifunctors is itself a Bifunctor */ + def compose[G[_, _]](implicit G0: Bifunctor[G]): Bifunctor[Lambda[(A, B) => F[G[A, B], G[A, B]]]] = + new CompositeBifunctor[F, G] { + val F = self + val G = G0 + } } object Bifunctor { def apply[F[_, _]](implicit ev: Bifunctor[F]): Bifunctor[F] = ev } + +trait CompositeBifunctor[F[_, _], G[_, _]] + extends Bifunctor[Lambda[(A, B) => F[G[A, B], G[A, B]]]] { + def F: Bifunctor[F] + def G: Bifunctor[G] + + def bimap[A, B, C, D](fab: F[G[A, B], G[A, B]])(f: A => C, g: B => D): F[G[C, D], G[C, D]] = { + val innerBimap: G[A, B] => G[C, D] = gab => G.bimap(gab)(f, g) + F.bimap(fab)(innerBimap, innerBimap) + } +} diff --git a/tests/src/test/scala/cats/tests/BifunctorTests.scala b/tests/src/test/scala/cats/tests/BifunctorTests.scala new file mode 100644 index 0000000000..b371e72e31 --- /dev/null +++ b/tests/src/test/scala/cats/tests/BifunctorTests.scala @@ -0,0 +1,14 @@ +package cats +package tests + +import cats.functor.Bifunctor +import cats.laws.discipline.{SerializableTests, BifunctorTests} + +class BifunctorTest extends CatsSuite { + type Tuple2Either[A, B] = (Either[A, B], Either[A, B]) + val tuple2ComposeEither: Bifunctor[Tuple2Either] = + Bifunctor[Tuple2].compose[Either] + + checkAll("Tuple2 compose Either", BifunctorTests(tuple2ComposeEither).bifunctor[Int, Int, Int, String, String, String]) + checkAll("Bifunctor[Tuple2 compose Either]", SerializableTests.serializable(tuple2ComposeEither)) +}