Skip to content

Commit

Permalink
Add Bitraverse, fixes #800
Browse files Browse the repository at this point in the history
  • Loading branch information
adelbertc committed Feb 25, 2016
1 parent 4820a0c commit 23550df
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 20 deletions.
45 changes: 45 additions & 0 deletions core/src/main/scala/cats/Bitraverse.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package cats

import cats.functor.Bifunctor

/**
* A type class abstracting over types that give rise to two independent [[cats.Traverse]]s.
*/
trait Bitraverse[F[_, _]] extends Bifoldable[F] with Bifunctor[F] { self =>
/** Traverse each side of the structure with the given functions */
def bitraverse[G[_]: Applicative, A, B, C, D](fab: F[A, B])(f: A => G[C], g: B => G[D]): G[F[C, D]]

/** Sequence each side of the structure with the given functions */
def bisequence[G[_]: Applicative, A, B](fab: F[G[A], G[B]]): G[F[A, B]] =
bitraverse(fab)(identity, identity)

/** If F and G are both [[cats.Bitraverse]] then so is their composition F[G[_, _], G[_, _]] */
def compose[G[_, _]](implicit ev: Bitraverse[G]): Bifoldable[Lambda[(A, B) => F[G[A, B], G[A, B]]]] =
new CompositeBitraverse[F, G] {
val F = self
val G = ev
}

override def bimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): F[C, D] =
bitraverse[Id, A, B, C, D](fab)(f, g)
}

object Bitraverse {
def apply[F[_, _]](implicit F: Bitraverse[F]): Bitraverse[F] = F
}

trait CompositeBitraverse[F[_, _], G[_, _]]
extends Bitraverse[Lambda[(A, B) => F[G[A, B], G[A, B]]]]
with CompositeBifoldable[F, G] {
def F: Bitraverse[F]
def G: Bitraverse[G]

def bitraverse[H[_]: Applicative, A, B, C, D](
fab: F[G[A, B], G[A, B]])(
f: A => H[C], g: B => H[D]
): H[F[G[C, D], G[C, D]]] =
F.bitraverse(fab)(
gab => G.bitraverse(gab)(f, g),
gab => G.bitraverse(gab)(f, g)
)
}
12 changes: 9 additions & 3 deletions core/src/main/scala/cats/data/Xor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -166,14 +166,20 @@ 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] 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)
implicit def xorBifunctor: Bitraverse[Xor] =
new Bitraverse[Xor] {
def bitraverse[G[_], A, B, C, D](fab: Xor[A, B])(f: A => G[C], g: B => G[D])(implicit G: Applicative[G]): G[Xor[C, D]] =
fab match {
case Xor.Left(a) => G.map(f(a))(Xor.left)
case Xor.Right(b) => G.map(g(b))(Xor.right)
}

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)
Expand Down
11 changes: 9 additions & 2 deletions core/src/main/scala/cats/std/either.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@ package cats
package std

trait EitherInstances extends EitherInstances1 {
implicit val eitherBifoldable: Bifoldable[Either] =
new Bifoldable[Either] {
implicit val eitherBitraverse: Bitraverse[Either] =
new Bitraverse[Either] {
def bitraverse[G[_], A, B, C, D](fab: Either[A, B])(f: A => G[C], g: B => G[D])(implicit G: Applicative[G]): G[Either[C, D]] =
fab match {
case Left(a) => G.map(f(a))(Left(_))
case Right(b) => G.map(g(b))(Right(_))
}

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)
Expand Down
7 changes: 5 additions & 2 deletions core/src/main/scala/cats/std/tuple.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ package std
trait TupleInstances extends Tuple2Instances

sealed trait Tuple2Instances {
implicit val tuple2Bifoldable: Bifoldable[Tuple2] =
new Bifoldable[Tuple2] {
implicit val tuple2Bitraverse: Bitraverse[Tuple2] =
new Bitraverse[Tuple2] {
def bitraverse[G[_]: Applicative, A, B, C, D](fab: (A, B))(f: A => G[C], g: B => G[D]): G[(C, D)] =
Applicative[G].tuple2(f(fab._1), g(fab._2))

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)

Expand Down
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 @@ -6,6 +6,7 @@ trait AllSyntax
with ApplySyntax
with BifunctorSyntax
with BifoldableSyntax
with BitraverseSyntax
with CartesianSyntax
with CoflatMapSyntax
with ComonadSyntax
Expand Down
15 changes: 15 additions & 0 deletions core/src/main/scala/cats/syntax/bitraverse.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package cats
package syntax

trait BitraverseSyntax {
implicit def bitraverseSyntax[F[_, _]: Bitraverse, A, B](fab: F[A, B]): BitraverseOps[F, A, B] =
new BitraverseOps[F, A, B](fab)
}

final class BitraverseOps[F[_, _], A, B](fab: F[A, B])(implicit F: Bitraverse[F]) {
def bitraverse[G[_]: Applicative, C, D](f: A => G[C], g: B => G[D]): G[F[C, D]] =
F.bitraverse(fab)(f, g)

def sequence[G[_], C, D](implicit G: Applicative[G], evLeft: A =:= G[C], evRight: B =:= G[D]): G[F[C, D]] =
F.bisequence(fab.asInstanceOf[F[G[C], G[D]]])
}
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 @@ -6,6 +6,7 @@ package object syntax {
object apply extends ApplySyntax
object bifunctor extends BifunctorSyntax
object bifoldable extends BifoldableSyntax
object bitraverse extends BitraverseSyntax
object cartesian extends CartesianSyntax
object coflatMap extends CoflatMapSyntax
object coproduct extends CoproductSyntax
Expand Down
38 changes: 38 additions & 0 deletions laws/src/main/scala/cats/laws/BitraverseLaws.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package cats
package laws

trait BitraverseLaws[F[_, _]] extends BifoldableLaws[F] with BifunctorLaws[F] {
implicit override def F: Bitraverse[F]

def bitraverseIdentity[A, B](fab: F[A, B]): IsEq[F[A, B]] =
fab <-> F.bitraverse[Id, A, B, A, B](fab)(identity, identity)

def bitraverseCompose[G[_], A, B, C, D, E, H](
fab: F[A, B],
f: A => G[C],
g: B => G[D],
h: C => G[E],
i: D => G[H]
)(implicit
G: Applicative[G]
): IsEq[G[G[F[E, H]]]] = {
val fg = F.bitraverse(fab)(f, g)
val hi = G.map(fg)(f => F.bitraverse(f)(h, i))

type GCompose[X] = G[G[X]]
val GCompose = G.compose[G]

val c =
F.bitraverse[GCompose, A, B, E, H](fab)(
a => G.map(f(a))(h),
b => G.map(g(b))(i)
)(GCompose)

hi <-> c
}
}

object BitraverseLaws {
def apply[F[_, _]](implicit ev: Bitraverse[F]): BitraverseLaws[F] =
new BitraverseLaws[F] { def F: Bitraverse[F] = ev }
}
47 changes: 47 additions & 0 deletions laws/src/main/scala/cats/laws/discipline/BitraverseTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package cats
package laws
package discipline

import org.scalacheck.Arbitrary
import org.scalacheck.Prop.forAll

trait BitraverseTests[F[_, _]] extends BifoldableTests[F] with BifunctorTests[F] {
def laws: BitraverseLaws[F]

def bitraverse[G[_], A, B, C, D, E, H](implicit
G: Applicative[G],
C: Monoid[C],
ArbFAB: Arbitrary[F[A, B]],
ArbFAD: Arbitrary[F[A, D]],
ArbGC: Arbitrary[G[C]],
ArbGD: Arbitrary[G[D]],
ArbGE: Arbitrary[G[E]],
ArbGH: Arbitrary[G[H]],
ArbA: Arbitrary[A],
ArbB: Arbitrary[B],
ArbC: Arbitrary[C],
ArbE: Arbitrary[E],
ArbH: Arbitrary[H],
EqFAB: Eq[F[A, B]],
EqFAD: Eq[F[A, D]],
EqFAH: Eq[F[A, H]],
EqFCD: Eq[F[C, D]],
EqFCH: Eq[F[C, H]],
EqGGFEH: Eq[G[G[F[E, H]]]],
EqC: Eq[C]
): RuleSet =
new RuleSet {
val name = "bitraverse"
val parents = Seq(bifoldable[A, B, C], bifunctor[A, B, C, D, E, H])
val bases = Seq.empty
val props = Seq(
"bitraverse identity" -> forAll(laws.bitraverseIdentity[A, B] _),
"bitraverse composition" -> forAll(laws.bitraverseCompose[G, A, B, C, D, E, H] _)
)
}
}

object BitraverseTests {
def apply[F[_, _]: Bitraverse]: BitraverseTests[F] =
new BitraverseTests[F] { def laws: BitraverseLaws[F] = BitraverseLaws[F] }
}
6 changes: 3 additions & 3 deletions 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.{BifoldableTests, TraverseTests, MonadTests, SerializableTests, CartesianTests}
import cats.laws.discipline.{BitraverseTests, TraverseTests, MonadTests, SerializableTests, CartesianTests}
import cats.laws.discipline.eq._
import algebra.laws.OrderLaws

Expand All @@ -18,8 +18,8 @@ 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]))
checkAll("Either[?, ?]", BitraverseTests[Either].bitraverse[Option, Int, Int, Int, String, String, String])
checkAll("Bitraverse[Either]", SerializableTests.serializable(Bitraverse[Either]))

val partialOrder = eitherPartialOrder[Int, String]
val order = implicitly[Order[Either[Int, String]]]
Expand Down
7 changes: 4 additions & 3 deletions tests/src/test/scala/cats/tests/TupleTests.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package cats
package tests

import cats.laws.discipline.{BifoldableTests, SerializableTests}
import cats.laws.discipline.{BitraverseTests, SerializableTests}
import cats.laws.discipline.eq.tuple2Eq

class TupleTests extends CatsSuite {
checkAll("Tuple2", BifoldableTests[Tuple2].bifoldable[Int, Int, Int])
checkAll("Bifoldable[Tuple2]", SerializableTests.serializable(Bifoldable[Tuple2]))
checkAll("Tuple2", BitraverseTests[Tuple2].bitraverse[Option, Int, Int, Int, String, String, String])
checkAll("Bitraverse[Tuple2]", SerializableTests.serializable(Bitraverse[Tuple2]))
}
10 changes: 3 additions & 7 deletions tests/src/test/scala/cats/tests/XorTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ 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, BifoldableTests, TraverseTests, MonadErrorTests, SerializableTests, CartesianTests}
import cats.laws.discipline.{BitraverseTests, 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,11 +54,8 @@ class XorTests extends CatsSuite {
} yield xor
}

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]))
checkAll("? Xor ?", BitraverseTests[Xor].bitraverse[Option, Int, Int, Int, String, String, String])
checkAll("Bitraverse[Xor]", SerializableTests.serializable(Bitraverse[Xor]))

test("catchOnly catches matching exceptions") {
assert(Xor.catchOnly[NumberFormatException]{ "foo".toInt }.isInstanceOf[Xor.Left[NumberFormatException]])
Expand Down

0 comments on commit 23550df

Please sign in to comment.