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 Bitraverse, fixes #800 #903

Merged
merged 2 commits into from
Feb 26, 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
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
22 changes: 22 additions & 0 deletions core/src/main/scala/cats/syntax/bitraverse.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package cats
package syntax

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

private[syntax] trait BitraverseSyntax1 {
implicit def nestedBitraverseSyntax[F[_, _]: Bitraverse, G[_], A, B](fgagb: F[G[A], G[B]]): NestedBitraverseOps[F, G, A, B] =
new NestedBitraverseOps[F, G, A, B](fgagb)
}

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)
}

final class NestedBitraverseOps[F[_, _], G[_], A, B](fgagb: F[G[A], G[B]])(implicit F: Bitraverse[F]) {
def bisequence(implicit G: Applicative[G]): G[F[A, B]] =
F.bisequence(fgagb)
}
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
11 changes: 11 additions & 0 deletions tests/src/test/scala/cats/tests/SyntaxTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,17 @@ class SyntaxTests extends AllInstances with AllSyntax {
val d0 = fab.bifoldMap(f2, g2)
}

def testBitraverse[F[_, _]: Bitraverse, G[_]: Applicative, A, B, C, D]: Unit = {
val f = mock[A => G[C]]
val g = mock[B => G[D]]

val fab = mock[F[A, B]]
val gfcd = fab.bitraverse(f, g)

val fgagb = mock[F[G[A], G[B]]]
val gfab = fgagb.bisequence
}

def testApplicative[F[_]: Applicative, A]: Unit = {
val a = mock[A]
val fa = a.pure[F]
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