Skip to content

Commit

Permalink
Add Bifoldable, fixes #94
Browse files Browse the repository at this point in the history
Also:
- Add MonadCombine separate
- Add serializability check for Bifunctor[Xor]
  • Loading branch information
adelbertc committed Feb 5, 2016
1 parent 2e9c100 commit e0f1c3c
Show file tree
Hide file tree
Showing 13 changed files with 166 additions and 4 deletions.
23 changes: 23 additions & 0 deletions core/src/main/scala/cats/Bifoldable.scala
Original file line number Diff line number Diff line change
@@ -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
}
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)
}
}
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))
}
}
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] }
}
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)
}
}
}
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 e0f1c3c

Please sign in to comment.