Skip to content

Commit

Permalink
Merge pull request #864 from adelbertc/bifoldable
Browse files Browse the repository at this point in the history
Add Bifoldable, fixes #94
  • Loading branch information
mpilquist committed Feb 17, 2016
2 parents 6dbd4b0 + a394388 commit 769efe7
Show file tree
Hide file tree
Showing 19 changed files with 237 additions and 4 deletions.
46 changes: 46 additions & 0 deletions core/src/main/scala/cats/Bifoldable.scala
Original file line number Diff line number Diff line change
@@ -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)
)
}
7 changes: 7 additions & 0 deletions core/src/main/scala/cats/MonadCombine.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
9 changes: 9 additions & 0 deletions core/src/main/scala/cats/data/Const.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 12 additions & 2 deletions core/src/main/scala/cats/data/Xor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand Down
1 change: 1 addition & 0 deletions core/src/main/scala/cats/std/all.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ trait AllInstances
with BigIntInstances
with BigDecimalInstances
with FutureInstances
with TupleInstances
14 changes: 14 additions & 0 deletions core/src/main/scala/cats/std/either.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/scala/cats/std/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ package object std {

object bigInt extends BigIntInstances
object bigDecimal extends BigDecimalInstances

object tuple extends TupleInstances
}
15 changes: 15 additions & 0 deletions core/src/main/scala/cats/std/tuple.scala
Original file line number Diff line number Diff line change
@@ -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))
}
}
1 change: 1 addition & 0 deletions core/src/main/scala/cats/syntax/all.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package syntax
trait AllSyntax
extends ApplySyntax
with BifunctorSyntax
with BifoldableSyntax
with CartesianSyntax
with CoflatMapSyntax
with ComonadSyntax
Expand Down
18 changes: 18 additions & 0 deletions core/src/main/scala/cats/syntax/bifoldable.scala
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions core/src/main/scala/cats/syntax/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions laws/src/main/scala/cats/laws/BifoldableLaws.scala
Original file line number Diff line number Diff line change
@@ -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
}
}
26 changes: 26 additions & 0 deletions laws/src/main/scala/cats/laws/discipline/BifoldableTests.scala
Original file line number Diff line number Diff line change
@@ -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] }
}
3 changes: 3 additions & 0 deletions tests/src/test/scala/cats/tests/ConstTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand Down
5 changes: 4 additions & 1 deletion tests/src/test/scala/cats/tests/EitherTests.scala
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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, ?]]]
Expand Down
18 changes: 18 additions & 0 deletions tests/src/test/scala/cats/tests/MonadCombineTests.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
16 changes: 16 additions & 0 deletions tests/src/test/scala/cats/tests/SyntaxTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
9 changes: 9 additions & 0 deletions tests/src/test/scala/cats/tests/TupleTests.scala
Original file line number Diff line number Diff line change
@@ -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]))
}
7 changes: 6 additions & 1 deletion tests/src/test/scala/cats/tests/XorTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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]])
Expand Down

0 comments on commit 769efe7

Please sign in to comment.