Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Bifoldable, fixes #94 #864

Merged
merged 4 commits into from
Feb 17, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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] =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we are combining the Bifunctor and Bifoldable instances in some places but not others. I guess if we are going to go forward with #800 then they will eventually be combined anyway. Was there any particular reason behind when they've been combined and when they haven't?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Bifunctor[Xor] instance was there prior to this PR. It wasn't in other places so I figured I would do those separately, hence #867. However because it looks like we have a use case for #800 I may abandon #867 and add Bitraverse once this is OK'ed and merged :-)

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will cause merge conflicts with #867, but I suppose we can just cross that bridge when we get to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it looks like we're doing #800 I'll be closing that


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