diff --git a/core/src/main/scala/cats/Bireducible.scala b/core/src/main/scala/cats/Bireducible.scala new file mode 100644 index 0000000000..906e006316 --- /dev/null +++ b/core/src/main/scala/cats/Bireducible.scala @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2015 Typelevel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package cats + +import cats.data.Ior + +trait Bireducible[F[_, _]] extends Bifoldable[F] { self => + + /** + * Left-reduce `F` by applying `ma` and `mb` to the "initial elements" of `fab` and combine them with + * every other value using the given functions `mca` and `mcb`. + * + * This method has to be implemented. + */ + def bireduceLeftTo[A, B, C](fab: F[A, B])( + ma: A => C, + mb: B => C + )( + mca: (C, A) => C, + mcb: (C, B) => C + ): C + + def bireduceRightTo[A, B, C](fab: F[A, B])( + ma: A => Eval[C], + mb: B => Eval[C] + )( + mac: (A, Eval[C]) => Eval[C], + mbc: (B, Eval[C]) => Eval[C] + ): Eval[C] + + def bireduceLeft[A, B](fab: F[A, B])(ma: (A, A) => A, mb: (B, B) => B): A Ior B = + Bireducible.bireduceLeft(fab)(ma, mb)(self) + + def bireduceRight[A, B](fab: F[A, B])( + ma: (A, Eval[A]) => Eval[A], + mb: (B, Eval[B]) => Eval[B] + ): Eval[A Ior B] = + Bireducible.bireduceRight(fab)(ma, mb)(self) + + /** + * Collapse the structure by mapping each element to an element of a type that has a [[cats.Semigroup]] + */ + def bireduceMap[A, B, C](fab: F[A, B])(ma: A => C, mb: B => C)(implicit C: Semigroup[C]): C = + Bireducible.bireduceMap(fab)(ma, mb)(self, C) + + def bireduce[A, B](fab: F[A, B])(implicit A: Semigroup[A], B: Semigroup[B]): A Ior B = + Bireducible.bireduce(fab)(self, A, B) + + def compose[G[_, _]](implicit ev: Bireducible[G]): Bireducible[λ[(α, β) => F[G[α, β], G[α, β]]]] = + new ComposedBireducible[F, G] { + override val F = self + override val G = ev + } +} + +object Bireducible { + + /** + * Summon an instance of [[Bireducible]]. + */ + @inline def apply[F[_, _]](implicit F: Bireducible[F]): Bireducible[F] = F + + /** Default implementation for [[cats.Bireducible#bireduceLeft]] based on [[cats.Bireducible#bireduceLeftTo]]. */ + private[cats] def bireduceLeft[F[_, _], A, B](fab: F[A, B])( + ma: (A, A) => A, + mb: (B, B) => B + )(implicit + F: Bireducible[F] + ): A Ior B = + F.bireduceLeftTo(fab)(Ior.left, Ior.right)( + (c, a) => c.addLeft(a)(ma(_, _)), + (c, b) => c.addRight(b)(mb(_, _)) + ) + + /** Default implementation for [[cats.Bireducible#bireduceRight]] based on [[cats.Bireducible#bireduceRightTo]]. */ + private[cats] def bireduceRight[F[_, _], A, B](fab: F[A, B])( + ma: (A, Eval[A]) => Eval[A], + mb: (B, Eval[B]) => Eval[B] + )(implicit + F: Bireducible[F] + ): Eval[A Ior B] = + F.bireduceRightTo(fab)( + { a => Eval.now(Ior.left(a)) }, + { b => Eval.now(Ior.right(b)) } + )( + { (a, ec) => + ec.flatMap { + case Ior.Left(aa) => ma(a, Eval.now(aa)).map(Ior.left) + case Ior.Right(bb) => Eval.now(Ior.both(a, bb)) + case Ior.Both(aa, bb) => ma(a, Eval.now(aa)).map(Ior.both(_, bb)) + } + }, + { (b, ec) => + ec.flatMap { + case Ior.Left(aa) => Eval.now(Ior.Both(aa, b)) + case Ior.Right(bb) => mb(b, Eval.now(bb)).map(Ior.right) + case Ior.Both(aa, bb) => mb(b, Eval.now(bb)).map(Ior.both(aa, _)) + } + } + ) + + /** Default implementation for [[cats.Bireducible#bireduceMap]] based on [[cats.Bireducible#bireduceLeftTo]]. */ + private[cats] def bireduceMap[F[A, B], A, B, C](fab: F[A, B])( + ma: A => C, + mb: B => C + )(implicit + F: Bireducible[F], + C: Semigroup[C] + ): C = + F.bireduceLeftTo(fab)(ma, mb)( + (c: C, a: A) => C.combine(c, ma(a)), + (c: C, b: B) => C.combine(c, mb(b)) + ) + + /** Default implementation for [[cats.Bireducible#bireduce]] based on [[cats.Bireducible#bireduceLeft]]. */ + private[cats] def bireduce[F[_, _], A, B](fab: F[A, B])(implicit + F: Bireducible[F], + A: Semigroup[A], + B: Semigroup[B] + ): A Ior B = + F.bireduceLeft(fab)(A.combine, B.combine) +} + +private[cats] trait ComposedBireducible[F[_, _], G[_, _]] + extends Bireducible[λ[(α, β) => F[G[α, β], G[α, β]]]] + with ComposedBifoldable[F, G] { + + implicit def F: Bireducible[F] + implicit def G: Bireducible[G] + + override def bireduceLeftTo[A, B, C](fgab: F[G[A, B], G[A, B]])( + ma: A => C, + mb: B => C + )( + mca: (C, A) => C, + mcb: (C, B) => C + ): C = { + def bireduceG(gab: G[A, B]): C = G.bireduceLeftTo(gab)(ma, mb)(mca, mcb) + def bifoldG(c: C, gab: G[A, B]): C = G.bifoldLeft(gab, c)(mca, mcb) + + F.bireduceLeftTo[G[A, B], G[A, B], C](fgab)(bireduceG, bireduceG)(bifoldG, bifoldG) + } + + override def bireduceRightTo[A, B, C](fgab: F[G[A, B], G[A, B]])( + ma: A => Eval[C], + mb: B => Eval[C] + )( + mac: (A, Eval[C]) => Eval[C], + mbc: (B, Eval[C]) => Eval[C] + ): Eval[C] = { + def bireduceG(gab: G[A, B]): Eval[C] = G.bireduceRightTo(gab)(ma, mb)(mac, mbc) + def bifoldG(gab: G[A, B], c: Eval[C]): Eval[C] = G.bifoldRight(gab, c)(mac, mbc) + + F.bireduceRightTo(fgab)(bireduceG, bireduceG)(bifoldG, bifoldG) + } +} diff --git a/laws/src/main/scala/cats/laws/BireducibleLaws.scala b/laws/src/main/scala/cats/laws/BireducibleLaws.scala new file mode 100644 index 0000000000..7387de9f0a --- /dev/null +++ b/laws/src/main/scala/cats/laws/BireducibleLaws.scala @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2015 Typelevel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package cats +package laws + +import cats.data.Ior +import cats.kernel.laws.IsEq + +trait BireducibleLaws[F[_, _]] extends BifoldableLaws[F] { + implicit def F: Bireducible[F] + + def bireduceLeftConsistentWithDefaultImplementation[A, B]( + fab: F[A, B], + ma: (A, A) => A, + mb: (B, B) => B + ): IsEq[A Ior B] = { + val obtained = F.bireduceLeft(fab)(ma, mb) + val expected = Bireducible.bireduceLeft(fab)(ma, mb) + + obtained <-> expected + } + + def bireduceRightConsistentWithDefaultImplementation[A, B]( + fab: F[A, B], + ma: (A, Eval[A]) => Eval[A], + mb: (B, Eval[B]) => Eval[B] + ): IsEq[A Ior B] = { + val obtained = F.bireduceRight(fab)(ma, mb).value + val expected = Bireducible.bireduceRight(fab)(ma, mb).value + + obtained <-> expected + } + + def bireduceMapConsistentWithDefaultImplementation[A, B, C]( + fab: F[A, B], + ma: A => C, + mb: B => C + )(implicit + C: Semigroup[C] + ): IsEq[C] = { + val obtained = F.bireduceMap(fab)(ma, mb) + val expected = Bireducible.bireduceMap(fab)(ma, mb) + + obtained <-> expected + } + + def bireduceConsistentWithDefaultImplementation[A, B]( + fab: F[A, B] + )(implicit A: Semigroup[A], B: Semigroup[B]): IsEq[A Ior B] = { + + val obtained = F.bireduce(fab) + val expected = Bireducible.bireduce(fab) + + obtained <-> expected + } + + def bireduceLeftToConsistentWithBireduceRightTo[A, B, C]( + fab: F[A, B], + ma: A => C, + mb: B => C, + mca: (C, A) => C, + mcb: (C, B) => C + ): IsEq[C] = { + val left = F.bireduceLeftTo(fab)(ma, mb)(mca, mcb) + val right = + F.bireduceRightTo(fab)( + a => Eval.now(ma(a)), + b => Eval.now(mb(b)) + )( + (a, c) => c.map(mca(_, a)), + (b, c) => c.map(mcb(_, b)) + ).value + + left <-> right + } +} + +object BireducibleLaws { + def apply[F[_, _]](implicit ev: Bireducible[F]): BireducibleLaws[F] = + new BireducibleLaws[F] { + def F: Bireducible[F] = ev + } +} diff --git a/laws/src/main/scala/cats/laws/discipline/BireducibleTests.scala b/laws/src/main/scala/cats/laws/discipline/BireducibleTests.scala new file mode 100644 index 0000000000..08f633fd55 --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/BireducibleTests.scala @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2015 Typelevel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package cats +package laws +package discipline + +import org.scalacheck.Arbitrary +import org.scalacheck.Cogen +import org.scalacheck.Prop._ + +import arbitrary._ + +trait BireducibleTests[F[_, _]] extends BifoldableTests[F] { + def laws: BireducibleLaws[F] + + def bireducible[ + A: Arbitrary: Cogen: Eq: Semigroup, + B: Arbitrary: Cogen: Eq: Semigroup, + C: Arbitrary: Cogen: Eq: Monoid + ](implicit arbF: Arbitrary[F[A, B]]): RuleSet = + new DefaultRuleSet( + "bireducible", + Some(bifoldable[A, B, C]), + "bireduceLeft consistent with default implementation" -> + forAll(laws.bireduceLeftConsistentWithDefaultImplementation[A, B] _), + "bireduceRight consistent with default implementation" -> + forAll(laws.bireduceRightConsistentWithDefaultImplementation[A, B] _), + "bireduceMap consistent with default implementation" -> + forAll(laws.bireduceMapConsistentWithDefaultImplementation[A, B, C] _), + "bireduce consistent with default implementation" -> + forAll(laws.bireduceConsistentWithDefaultImplementation[A, B] _), + "bireduceLeftTo consistent with bireduceRightTo" -> + forAll(laws.bireduceLeftToConsistentWithBireduceRightTo[A, B, C] _) + ) +} + +object BireducibleTests { + def apply[F[_, _]: Bireducible]: BireducibleTests[F] = + new BireducibleTests[F] { def laws: BireducibleLaws[F] = BireducibleLaws[F] } +} diff --git a/tests/shared/src/test/scala/cats/tests/BireducibleSuite.scala b/tests/shared/src/test/scala/cats/tests/BireducibleSuite.scala new file mode 100644 index 0000000000..1c95f71b0f --- /dev/null +++ b/tests/shared/src/test/scala/cats/tests/BireducibleSuite.scala @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2015 Typelevel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package cats +package tests + +import cats.data.Ior +import cats.data.NonEmptyList +import cats.laws.discipline.arbitrary._ +import cats.syntax.reducible._ +import org.scalacheck.Prop + +class BireducibleSuite extends CatsSuite { + import BireducibleSuite._ + + // Bireducible.bireduceLeft tests + // + property("Bireducible.bireduceLeft[IorNel]") { + Prop.forAll { (testee: NonEmptyList[String Ior List[Byte]]) => + val expected = testee.reduce + val obtained = Bireducible.bireduceLeft(IorNel(testee))(_ + _, _ ::: _) + + assertEquals(obtained, expected) + } + } + property("Bireducible.bireduceLeft[NelIor]") { + Prop.forAll { (testee: NonEmptyList[List[Char]] Ior NonEmptyList[String]) => + val expected = testee.bimap(_.reduce, _.reduce) + val obtained = Bireducible.bireduceLeft(NelIor(testee))(_ ::: _, _ + _) + + assertEquals(obtained, expected) + } + } + // Bireducible.bireduceRight tests + // + property("Bireducible.bireduceRight[IorNel]") { + Prop.forAll { (testee: NonEmptyList[String Ior List[Char]]) => + val expected = testee.reduce + val obtained = + Bireducible + .bireduceRight(IorNel(testee))( + (a, ea) => ea.map(a + _), + (b, eb) => eb.map(b ::: _) + ) + .value + + assertEquals(obtained, expected) + } + } + property("Bireducible.bireduceRight[NelIor]") { + Prop.forAll { (testee: NonEmptyList[List[Byte]] Ior NonEmptyList[String]) => + val expected = testee.bimap(_.reduce, _.reduce) + val obtained = + Bireducible + .bireduceRight(NelIor(testee))( + (a, ea) => ea.map(a ::: _), + (b, eb) => eb.map(b + _) + ) + .value + + assertEquals(obtained, expected) + } + } + // Bireducible.bireduceMap tests + // + property("Bireducible.bireduceMap[IorNel]") { + Prop.forAll { (testee: NonEmptyList[String Ior String]) => + val expected = testee.map(_.merge).reduce + val obtained = Bireducible.bireduceMap(IorNel(testee))(identity, identity) + + assertNoDiff(obtained, expected) + } + } + property("Bireducible.bireduceMap[NelIor]") { + Prop.forAll { (testee: NonEmptyList[String] Ior NonEmptyList[String]) => + val expected = testee.bimap(_.reduce, _.reduce).merge + val obtained = Bireducible.bireduceMap(NelIor(testee))(identity, identity) + + assertNoDiff(obtained, expected) + } + } + // Bireducible.bireduce tests + // + property("Bireducible.bireduce[IorNel]") { + Prop.forAll { (testee: NonEmptyList[List[Byte] Ior List[Char]]) => + val expected = testee.reduce + val obtained = Bireducible.bireduce(IorNel(testee)) + + assertEquals(obtained, expected) + } + } + property("Bireducible.bireduce[NelIor]") { + Prop.forAll { (testee: NonEmptyList[List[Char]] Ior NonEmptyList[List[Byte]]) => + val expected = testee.bimap(_.reduce, _.reduce) + val obtained = Bireducible.bireduce(NelIor(testee)) + + assertEquals(obtained, expected) + } + } +} + +object BireducibleSuite { + + /** `Bireducible` that can be collapsed row by row. + */ + final case class IorNel[A, B](value: NonEmptyList[A Ior B]) extends AnyVal + + /** `Bireducible` that can be collapsed by collapsing one of the columns followed by the other one. + */ + final case class NelIor[A, B](value: NonEmptyList[A] Ior NonEmptyList[B]) extends AnyVal + + implicit val testIorNelBireducible: Bireducible[IorNel] = new Bireducible[IorNel] { + + override def bifoldLeft[A, B, C](fab: IorNel[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C = ??? + + override def bifoldRight[A, B, C](fab: IorNel[A, B], c: Eval[C])( + f: (A, Eval[C]) => Eval[C], + g: (B, Eval[C]) => Eval[C] + ): Eval[C] = ??? + + override def bireduceLeftTo[A, B, C](fab: IorNel[A, B])( + ma: A => C, + mb: B => C + )( + mca: (C, A) => C, + mcb: (C, B) => C + ): C = { + fab.value.reduceLeftTo { ab => + ab.fold(ma, mb, (a, b) => mcb(ma(a), b)) + } { (c, ab) => + ab.fold(mca(c, _), mcb(c, _), (a, b) => mcb(mca(c, a), b)) + } + } + + override def bireduceRightTo[A, B, C]( + fab: IorNel[A, B] + )( + ma: A => Eval[C], + mb: B => Eval[C] + )( + mac: (A, Eval[C]) => Eval[C], + mbc: (B, Eval[C]) => Eval[C] + ): Eval[C] = + fab.value.reduceRightTo { ab => + // Enforcing `.value` here because this parameter is defined as `A => B`. + // TODO: consider making `reduceRightTo` to take `A => Eval[B]` to improve its composability. + ab.fold(ma, mb, (a, b) => mbc(b, ma(a))).value + } { (ab, ec) => + ab.fold(a => mac(a, ec), b => mbc(b, ec), (a, b) => mbc(b, mac(a, ec))) + } + } + + implicit val testNelIorBireducible: Bireducible[NelIor] = new Bireducible[NelIor] { + + override def bifoldLeft[A, B, C](fab: NelIor[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C = ??? + + override def bifoldRight[A, B, C](fab: NelIor[A, B], c: Eval[C])( + f: (A, Eval[C]) => Eval[C], + g: (B, Eval[C]) => Eval[C] + ): Eval[C] = ??? + + override def bireduceLeftTo[A, B, C](fab: NelIor[A, B])( + ma: A => C, + mb: B => C + )( + mca: (C, A) => C, + mcb: (C, B) => C + ): C = + fab.value.fold( + _.reduceLeftTo(ma)(mca), + _.reduceLeftTo(mb)(mcb), + { (as, bs) => + val ca = as.reduceLeftTo(ma)(mca) + val cb = bs.foldLeft(ca)(mcb) + cb + } + ) + + override def bireduceRightTo[A, B, C](fab: NelIor[A, B])( + ma: A => Eval[C], + mb: B => Eval[C] + )( + mac: (A, Eval[C]) => Eval[C], + mbc: (B, Eval[C]) => Eval[C] + ): Eval[C] = + fab.value.fold( + _.reduceRightTo(ma(_).value)(mac), + _.reduceRightTo(mb(_).value)(mbc), + { (as, bs) => + val ca = as.reduceRightTo(ma(_).value)(mac) + val cb = bs.foldRight(ca)(mbc) + cb + } + ) + } +}