diff --git a/core/src/main/scala/cats/Bifoldable.scala b/core/src/main/scala/cats/Bifoldable.scala new file mode 100644 index 0000000000..a4efac2a1b --- /dev/null +++ b/core/src/main/scala/cats/Bifoldable.scala @@ -0,0 +1,46 @@ +package cats + +/** + * A type class abstracting over types that give rise to two independent [[cats.Foldable]]s. + */ +trait Bifoldable[F[_, _]] extends Any with Serializable { self => + /** Collapse the structure with a left-associative function */ + def bifoldLeft[A, B, C](fab: F[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C + + /** Collapse the structure with a right-associative function */ + def bifoldRight[A, B, C](fab: F[A, B], c: Eval[C])(f: (A, Eval[C]) => Eval[C], g: (B, Eval[C]) => Eval[C]): Eval[C] + + /** Collapse the structure by mapping each element to an element of a type that has a [[cats.Monoid]] */ + def bifoldMap[A, B, C](fab: F[A, B])(f: A => C, g: B => C)(implicit C: Monoid[C]): C = + bifoldLeft(fab, C.empty)( + (c: C, a: A) => C.combine(c, f(a)), + (c: C, b: B) => C.combine(c, g(b)) + ) + + def compose[G[_, _]](implicit ev: Bifoldable[G]): Bifoldable[Lambda[(A, B) => F[G[A, B], G[A, B]]]] = + new CompositeBifoldable[F, G] { + val F = self + val G = ev + } +} + +object Bifoldable { + def apply[F[_, _]](implicit F: Bifoldable[F]): Bifoldable[F] = F +} + +trait CompositeBifoldable[F[_, _], G[_, _]] extends Bifoldable[Lambda[(A, B) => F[G[A, B], G[A, B]]]] { + implicit def F: Bifoldable[F] + implicit def G: Bifoldable[G] + + def bifoldLeft[A, B, C](fab: F[G[A, B], G[A, B]], c: C)(f: (C, A) => C, g: (C, B) => C): C = + F.bifoldLeft(fab, c)( + (c: C, gab: G[A, B]) => G.bifoldLeft(gab, c)(f, g), + (c: C, gab: G[A, B]) => G.bifoldLeft(gab, c)(f, g) + ) + + def bifoldRight[A, B, C](fab: F[G[A, B], G[A, B]], c: Eval[C])(f: (A, Eval[C]) => Eval[C], g: (B, Eval[C]) => Eval[C]): Eval[C] = + F.bifoldRight(fab, c)( + (gab: G[A, B], c: Eval[C]) => G.bifoldRight(gab, c)(f, g), + (gab: G[A, B], c: Eval[C]) => G.bifoldRight(gab, c)(f, g) + ) +} diff --git a/core/src/main/scala/cats/MonadCombine.scala b/core/src/main/scala/cats/MonadCombine.scala index e57729eafd..55cb76184e 100644 --- a/core/src/main/scala/cats/MonadCombine.scala +++ b/core/src/main/scala/cats/MonadCombine.scala @@ -18,4 +18,11 @@ import simulacrum.typeclass flatMap(fga) { ga => G.foldLeft(ga, empty[A])((acc, a) => combineK(acc, pure(a))) } + + /** Separate the inner foldable values into the "lefts" and "rights" */ + def separate[G[_, _], A, B](fgab: F[G[A, B]])(implicit G: Bifoldable[G]): (F[A], F[B]) = { + val as = flatMap(fgab)(gab => G.bifoldMap(gab)(pure, _ => empty[A])(algebra[A])) + val bs = flatMap(fgab)(gab => G.bifoldMap(gab)(_ => empty[B], pure)(algebra[B])) + (as, bs) + } } diff --git a/core/src/main/scala/cats/data/Const.scala b/core/src/main/scala/cats/data/Const.scala index 368e409838..692f6436f6 100644 --- a/core/src/main/scala/cats/data/Const.scala +++ b/core/src/main/scala/cats/data/Const.scala @@ -69,6 +69,15 @@ private[data] sealed abstract class ConstInstances extends ConstInstances0 { def combine(x: Const[A, B], y: Const[A, B]): Const[A, B] = x combine y } + + implicit val constBifoldable: Bifoldable[Const] = + new Bifoldable[Const] { + def bifoldLeft[A, B, C](fab: Const[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C = + f(c, fab.getConst) + + def bifoldRight[A, B, C](fab: Const[A, B], c: Eval[C])(f: (A, Eval[C]) => Eval[C], g: (B, Eval[C]) => Eval[C]): Eval[C] = + f(fab.getConst, c) + } } private[data] sealed abstract class ConstInstances0 extends ConstInstances1 { diff --git a/core/src/main/scala/cats/data/Xor.scala b/core/src/main/scala/cats/data/Xor.scala index 4333775c30..28e4066b90 100644 --- a/core/src/main/scala/cats/data/Xor.scala +++ b/core/src/main/scala/cats/data/Xor.scala @@ -166,9 +166,19 @@ 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] = - new Bifunctor[Xor] { + 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) + 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) + case Xor.Right(b) => g(b, c) + } } implicit def xorInstances[A]: Traverse[A Xor ?] with MonadError[Xor[A, ?], A] = diff --git a/core/src/main/scala/cats/std/all.scala b/core/src/main/scala/cats/std/all.scala index 5983aa0c5d..0060a5f513 100644 --- a/core/src/main/scala/cats/std/all.scala +++ b/core/src/main/scala/cats/std/all.scala @@ -15,3 +15,4 @@ trait AllInstances with BigIntInstances with BigDecimalInstances with FutureInstances + with TupleInstances diff --git a/core/src/main/scala/cats/std/either.scala b/core/src/main/scala/cats/std/either.scala index 18ebc138a1..8e21c3cdb7 100644 --- a/core/src/main/scala/cats/std/either.scala +++ b/core/src/main/scala/cats/std/either.scala @@ -2,6 +2,20 @@ package cats package std trait EitherInstances extends EitherInstances1 { + implicit val eitherBifoldable: Bifoldable[Either] = + new Bifoldable[Either] { + 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) + case Right(b) => g(b, c) + } + } + implicit def eitherInstances[A]: Monad[Either[A, ?]] with Traverse[Either[A, ?]] = new Monad[Either[A, ?]] with Traverse[Either[A, ?]] { def pure[B](b: B): Either[A, B] = Right(b) diff --git a/core/src/main/scala/cats/std/package.scala b/core/src/main/scala/cats/std/package.scala index 4e298d622c..9b31e87b51 100644 --- a/core/src/main/scala/cats/std/package.scala +++ b/core/src/main/scala/cats/std/package.scala @@ -27,4 +27,6 @@ package object std { object bigInt extends BigIntInstances object bigDecimal extends BigDecimalInstances + + object tuple extends TupleInstances } diff --git a/core/src/main/scala/cats/std/tuple.scala b/core/src/main/scala/cats/std/tuple.scala new file mode 100644 index 0000000000..65f55b419a --- /dev/null +++ b/core/src/main/scala/cats/std/tuple.scala @@ -0,0 +1,15 @@ +package cats +package std + +trait TupleInstances extends Tuple2Instances + +sealed trait Tuple2Instances { + implicit val tuple2Bifoldable: Bifoldable[Tuple2] = + new Bifoldable[Tuple2] { + 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) + + def bifoldRight[A, B, C](fab: (A, B), c: Eval[C])(f: (A, Eval[C]) => Eval[C], g: (B, Eval[C]) => Eval[C]): Eval[C] = + g(fab._2, f(fab._1, c)) + } +} diff --git a/core/src/main/scala/cats/syntax/all.scala b/core/src/main/scala/cats/syntax/all.scala index 269b210859..24e9274b35 100644 --- a/core/src/main/scala/cats/syntax/all.scala +++ b/core/src/main/scala/cats/syntax/all.scala @@ -4,6 +4,7 @@ package syntax trait AllSyntax extends ApplySyntax with BifunctorSyntax + with BifoldableSyntax with CartesianSyntax with CoflatMapSyntax with ComonadSyntax diff --git a/core/src/main/scala/cats/syntax/bifoldable.scala b/core/src/main/scala/cats/syntax/bifoldable.scala new file mode 100644 index 0000000000..698695c275 --- /dev/null +++ b/core/src/main/scala/cats/syntax/bifoldable.scala @@ -0,0 +1,18 @@ +package cats +package syntax + +trait BifoldableSyntax { + implicit def bifoldableSyntax[F[_, _]: Bifoldable, A, B](fab: F[A, B]): BifoldableOps[F, A, B] = + new BifoldableOps[F, A, B](fab) +} + +final class BifoldableOps[F[_, _], A, B](fab: F[A, B])(implicit F: Bifoldable[F]) { + def bifoldLeft[C](c: C)(f: (C, A) => C, g: (C, B) => C): C = + F.bifoldLeft(fab, c)(f, g) + + def bifoldRight[C](c: Eval[C])(f: (A, Eval[C]) => Eval[C], g: (B, Eval[C]) => Eval[C]): Eval[C] = + F.bifoldRight(fab, c)(f, g) + + def bifoldMap[C](f: A => C, g: B => C)(implicit C: Monoid[C]): C = + F.bifoldMap(fab)(f, g) +} diff --git a/core/src/main/scala/cats/syntax/package.scala b/core/src/main/scala/cats/syntax/package.scala index b592021184..462ea3e0ff 100644 --- a/core/src/main/scala/cats/syntax/package.scala +++ b/core/src/main/scala/cats/syntax/package.scala @@ -4,6 +4,7 @@ package object syntax { object all extends AllSyntax object apply extends ApplySyntax object bifunctor extends BifunctorSyntax + object bifoldable extends BifoldableSyntax object cartesian extends CartesianSyntax object coflatMap extends CoflatMapSyntax object coproduct extends CoproductSyntax diff --git a/laws/src/main/scala/cats/laws/BifoldableLaws.scala b/laws/src/main/scala/cats/laws/BifoldableLaws.scala new file mode 100644 index 0000000000..783eefa7c0 --- /dev/null +++ b/laws/src/main/scala/cats/laws/BifoldableLaws.scala @@ -0,0 +1,29 @@ +package cats +package laws + +trait BifoldableLaws[F[_, _]] { + implicit def F: Bifoldable[F] + + def bifoldLeftConsistentWithBifoldMap[A, B, C](fab: F[A, B], f: A => C, g: B => C)(implicit C: Monoid[C]): IsEq[C] = { + val expected = F.bifoldLeft(fab, C.empty)( + (c: C, a: A) => C.combine(c, f(a)), + (c: C, b: B) => C.combine(c, g(b)) + ) + expected <-> F.bifoldMap(fab)(f, g) + } + + def bifoldRightConsistentWithBifoldMap[A, B, C](fab: F[A, B], f: A => C, g: B => C)(implicit C: Monoid[C]): IsEq[C] = { + val expected = F.bifoldRight(fab, Later(C.empty))( + (a: A, ec: Eval[C]) => ec.map(c => C.combine(f(a), c)), + (b: B, ec: Eval[C]) => ec.map(c => C.combine(g(b), c)) + ) + expected.value <-> F.bifoldMap(fab)(f, g) + } +} + +object BifoldableLaws { + def apply[F[_, _]](implicit ev: Bifoldable[F]): BifoldableLaws[F] = + new BifoldableLaws[F] { + def F: Bifoldable[F] = ev + } +} diff --git a/laws/src/main/scala/cats/laws/discipline/BifoldableTests.scala b/laws/src/main/scala/cats/laws/discipline/BifoldableTests.scala new file mode 100644 index 0000000000..ec594702b8 --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/BifoldableTests.scala @@ -0,0 +1,26 @@ +package cats +package laws +package discipline + +import org.scalacheck.Arbitrary +import org.scalacheck.Prop._ +import org.typelevel.discipline.Laws + +trait BifoldableTests[F[_, _]] extends Laws { + def laws: BifoldableLaws[F] + + def bifoldable[A: Arbitrary, B: Arbitrary, C: Arbitrary: Monoid: Eq](implicit + ArbFAB: Arbitrary[F[A, B]] + ): RuleSet = + new DefaultRuleSet( + name = "bifoldable", + parent = None, + "bifoldLeft consistent with bifoldMap" -> forAll(laws.bifoldLeftConsistentWithBifoldMap[A, B, C] _), + "bifoldRight consistent with bifoldMap" -> forAll(laws.bifoldRightConsistentWithBifoldMap[A, B, C] _) + ) +} + +object BifoldableTests { + def apply[F[_, _]: Bifoldable]: BifoldableTests[F] = + new BifoldableTests[F] { def laws: BifoldableLaws[F] = BifoldableLaws[F] } +} diff --git a/tests/src/test/scala/cats/tests/ConstTests.scala b/tests/src/test/scala/cats/tests/ConstTests.scala index b4287fecb2..20023b2735 100644 --- a/tests/src/test/scala/cats/tests/ConstTests.scala +++ b/tests/src/test/scala/cats/tests/ConstTests.scala @@ -45,6 +45,9 @@ class ConstTests extends CatsSuite { checkAll("Const[String, Int]", ContravariantTests[Const[String, ?]].contravariant[Int, Int, Int]) checkAll("Contravariant[Const[String, ?]]", SerializableTests.serializable(Contravariant[Const[String, ?]])) + checkAll("Const[?, ?]", BifoldableTests[Const].bifoldable[Int, Int, Int]) + checkAll("Bifoldable[Const]", SerializableTests.serializable(Bifoldable[Const])) + test("show") { Const(1).show should === ("Const(1)") diff --git a/tests/src/test/scala/cats/tests/EitherTests.scala b/tests/src/test/scala/cats/tests/EitherTests.scala index 1c46f7ae95..7ae9050340 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.{TraverseTests, MonadTests, SerializableTests, CartesianTests} +import cats.laws.discipline.{BifoldableTests, TraverseTests, MonadTests, SerializableTests, CartesianTests} import cats.laws.discipline.eq._ import algebra.laws.OrderLaws @@ -18,6 +18,9 @@ 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])) + val partialOrder = eitherPartialOrder[Int, String] val order = implicitly[Order[Either[Int, String]]] val monad = implicitly[Monad[Either[Int, ?]]] diff --git a/tests/src/test/scala/cats/tests/MonadCombineTests.scala b/tests/src/test/scala/cats/tests/MonadCombineTests.scala new file mode 100644 index 0000000000..adf8f7df73 --- /dev/null +++ b/tests/src/test/scala/cats/tests/MonadCombineTests.scala @@ -0,0 +1,18 @@ +package cats +package tests + +import cats.data.Xor +import cats.laws.discipline.arbitrary.xorArbitrary +import cats.laws.discipline.eq.tuple2Eq + +class MonadCombineTest extends CatsSuite { + test("separate") { + forAll { (list: List[Xor[Int, String]]) => + val ints = list.collect { case Xor.Left(i) => i } + val strings = list.collect { case Xor.Right(s) => s } + val expected = (ints, strings) + + MonadCombine[List].separate(list) should === (expected) + } + } +} diff --git a/tests/src/test/scala/cats/tests/SyntaxTests.scala b/tests/src/test/scala/cats/tests/SyntaxTests.scala index 73ca9a7f2d..a052a80d51 100644 --- a/tests/src/test/scala/cats/tests/SyntaxTests.scala +++ b/tests/src/test/scala/cats/tests/SyntaxTests.scala @@ -185,4 +185,20 @@ class SyntaxTests extends AllInstances with AllSyntax { val fz4: F[Z] = (fa |@| fb |@| fc).map(f2) val fz5: F[Z] = (fa |@| fb |@| fc).apWith(ff2) } + + def testBifoldable[F[_, _]: Bifoldable, A, B, C, D: Monoid]: Unit = { + val fab = mock[F[A, B]] + + val f0 = mock[(C, A) => C] + val g0 = mock[(C, B) => C] + val c0 = fab.bifoldLeft(mock[C])(f0, g0) + + val f1 = mock[(A, Eval[C]) => Eval[C]] + val g1 = mock[(B, Eval[C]) => Eval[C]] + val c1 = fab.bifoldRight(mock[Eval[C]])(f1, g1) + + val f2 = mock[A => D] + val g2 = mock[B => D] + val d0 = fab.bifoldMap(f2, g2) + } } diff --git a/tests/src/test/scala/cats/tests/TupleTests.scala b/tests/src/test/scala/cats/tests/TupleTests.scala new file mode 100644 index 0000000000..066c52f00e --- /dev/null +++ b/tests/src/test/scala/cats/tests/TupleTests.scala @@ -0,0 +1,9 @@ +package cats +package tests + +import cats.laws.discipline.{BifoldableTests, SerializableTests} + +class TupleTests extends CatsSuite { + checkAll("Tuple2", BifoldableTests[Tuple2].bifoldable[Int, Int, Int]) + checkAll("Bifoldable[Tuple2]", SerializableTests.serializable(Bifoldable[Tuple2])) +} diff --git a/tests/src/test/scala/cats/tests/XorTests.scala b/tests/src/test/scala/cats/tests/XorTests.scala index cffa23ba8c..ec6d0256a0 100644 --- a/tests/src/test/scala/cats/tests/XorTests.scala +++ b/tests/src/test/scala/cats/tests/XorTests.scala @@ -3,8 +3,9 @@ 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, TraverseTests, MonadErrorTests, SerializableTests, CartesianTests} +import cats.laws.discipline.{BifunctorTests, BifoldableTests, TraverseTests, MonadErrorTests, SerializableTests, CartesianTests} import cats.laws.discipline.eq.tuple3Eq import algebra.laws.{GroupLaws, OrderLaws} import org.scalacheck.{Arbitrary, Gen} @@ -55,6 +56,10 @@ class XorTests extends CatsSuite { } 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])) test("catchOnly catches matching exceptions") { assert(Xor.catchOnly[NumberFormatException]{ "foo".toInt }.isInstanceOf[Xor.Left[NumberFormatException]])