From e0f1c3c2965e675a629b3c4dbefbe61d1a70781e Mon Sep 17 00:00:00 2001 From: Adelbert Chang Date: Fri, 5 Feb 2016 00:21:49 -0800 Subject: [PATCH] Add Bifoldable, fixes #94 Also: - Add MonadCombine separate - Add serializability check for Bifunctor[Xor] --- core/src/main/scala/cats/Bifoldable.scala | 23 +++++++++++++++ core/src/main/scala/cats/MonadCombine.scala | 7 +++++ core/src/main/scala/cats/data/Xor.scala | 14 +++++++-- core/src/main/scala/cats/std/all.scala | 1 + core/src/main/scala/cats/std/either.scala | 14 +++++++++ core/src/main/scala/cats/std/package.scala | 2 ++ core/src/main/scala/cats/std/tuple.scala | 15 ++++++++++ .../main/scala/cats/laws/BifoldableLaws.scala | 29 +++++++++++++++++++ .../laws/discipline/BifoldableTests.scala | 26 +++++++++++++++++ .../test/scala/cats/tests/EitherTests.scala | 5 +++- .../scala/cats/tests/MonadCombineTests.scala | 18 ++++++++++++ .../test/scala/cats/tests/TupleTests.scala | 9 ++++++ .../src/test/scala/cats/tests/XorTests.scala | 7 ++++- 13 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 core/src/main/scala/cats/Bifoldable.scala create mode 100644 core/src/main/scala/cats/std/tuple.scala create mode 100644 laws/src/main/scala/cats/laws/BifoldableLaws.scala create mode 100644 laws/src/main/scala/cats/laws/discipline/BifoldableTests.scala create mode 100644 tests/src/test/scala/cats/tests/MonadCombineTests.scala create mode 100644 tests/src/test/scala/cats/tests/TupleTests.scala diff --git a/core/src/main/scala/cats/Bifoldable.scala b/core/src/main/scala/cats/Bifoldable.scala new file mode 100644 index 0000000000..a11ee02369 --- /dev/null +++ b/core/src/main/scala/cats/Bifoldable.scala @@ -0,0 +1,23 @@ +package cats + +/** + * A type class abstracting over types that give rise to two independent [[cats.Foldable]]s. + */ +trait Bifoldable[F[_, _]] extends Serializable { + /** 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)) + ) +} + +object Bifoldable { + def apply[F[_, _]](implicit F: Bifoldable[F]): Bifoldable[F] = F +} diff --git a/core/src/main/scala/cats/MonadCombine.scala b/core/src/main/scala/cats/MonadCombine.scala index e57729eafd..979faf02f8 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/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/laws/src/main/scala/cats/laws/BifoldableLaws.scala b/laws/src/main/scala/cats/laws/BifoldableLaws.scala new file mode 100644 index 0000000000..5f21b94cba --- /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/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/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]])