diff --git a/core/src/main/scala/cats/data/Ior.scala b/core/src/main/scala/cats/data/Ior.scala index fd73f9953b..fc3270b5f0 100644 --- a/core/src/main/scala/cats/data/Ior.scala +++ b/core/src/main/scala/cats/data/Ior.scala @@ -1,6 +1,8 @@ package cats package data +import cats.functor.Bifunctor + /** Represents a right-biased disjunction that is either an `A`, or a `B`, or both an `A` and a `B`. * * An instance of `A [[Ior]] B` is one of: @@ -142,6 +144,11 @@ sealed abstract class IorInstances extends IorInstances0 { def pure[B](b: B): A Ior B = Ior.right(b) def flatMap[B, C](fa: A Ior B)(f: B => A Ior C): A Ior C = fa.flatMap(f) } + + implicit def iorBifunctor: Bifunctor[Ior] = + new Bifunctor[Ior] { + override def bimap[A, B, C, D](fab: A Ior B)(f: A => C, g: B => D): C Ior D = fab.bimap(f, g) + } } sealed abstract class IorInstances0 { diff --git a/core/src/main/scala/cats/data/Validated.scala b/core/src/main/scala/cats/data/Validated.scala index 990f8da019..7e135c4374 100644 --- a/core/src/main/scala/cats/data/Validated.scala +++ b/core/src/main/scala/cats/data/Validated.scala @@ -1,9 +1,11 @@ package cats package data +import cats.data.Validated.{Invalid, Valid} +import cats.functor.Bifunctor + import scala.reflect.ClassTag -import cats.data.Validated.{Valid, Invalid} -import scala.util.{Success, Failure, Try} +import scala.util.{Failure, Success, Try} sealed abstract class Validated[+E, +A] extends Product with Serializable { @@ -172,6 +174,11 @@ sealed abstract class ValidatedInstances extends ValidatedInstances1 { def show(f: Validated[A,B]): String = f.show } + implicit def validatedBifunctor: Bifunctor[Validated] = + new Bifunctor[Validated] { + override def bimap[A, B, C, D](fab: Validated[A, B])(f: A => C, g: B => D): Validated[C, D] = fab.bimap(f, g) + } + implicit def validatedInstances[E](implicit E: Semigroup[E]): Traverse[Validated[E, ?]] with Applicative[Validated[E, ?]] = new Traverse[Validated[E, ?]] with Applicative[Validated[E,?]] { def traverse[F[_]: Applicative, A, B](fa: Validated[E,A])(f: A => F[B]): F[Validated[E,B]] = diff --git a/core/src/main/scala/cats/data/Xor.scala b/core/src/main/scala/cats/data/Xor.scala index 232d782fef..f9a0af1f4d 100644 --- a/core/src/main/scala/cats/data/Xor.scala +++ b/core/src/main/scala/cats/data/Xor.scala @@ -1,6 +1,8 @@ package cats package data +import cats.functor.Bifunctor + import scala.reflect.ClassTag import scala.util.{Failure, Success, Try} @@ -162,6 +164,12 @@ 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] { + 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 xorInstances[A]: Traverse[A Xor ?] with MonadError[Xor, A ] = new Traverse[A Xor ?] with MonadError[Xor, A] { def traverse[F[_]: Applicative, B, C](fa: A Xor B)(f: B => F[C]): F[A Xor C] = fa.traverse(f) diff --git a/core/src/main/scala/cats/data/XorT.scala b/core/src/main/scala/cats/data/XorT.scala index 2adc1efe46..bdb5060f88 100644 --- a/core/src/main/scala/cats/data/XorT.scala +++ b/core/src/main/scala/cats/data/XorT.scala @@ -1,6 +1,8 @@ package cats package data +import cats.functor.Bifunctor + /** * Transformer for `Xor`, allowing the effect of an arbitrary type constructor `F` to be combined with the * fail-fast effect of `Xor`. @@ -156,6 +158,13 @@ abstract class XorTInstances extends XorTInstances1 { implicit def xorTShow[F[_], L, R](implicit sh: Show[F[L Xor R]]): Show[XorT[F, L, R]] = functor.Contravariant[Show].contramap(sh)(_.value) + + implicit def xorTBifunctor[F[_]](implicit F: Functor[F]): Bifunctor[XorT[F, ?, ?]] = { + new Bifunctor[XorT[F, ?, ?]] { + override def bimap[A, B, C, D](fab: XorT[F, A, B])(f: A => C, g: B => D): XorT[F, C, D] = fab.bimap(f, g) + } + } + } private[data] abstract class XorTInstances1 extends XorTInstances2 { diff --git a/laws/src/main/scala/cats/laws/BifunctorLaws.scala b/laws/src/main/scala/cats/laws/BifunctorLaws.scala new file mode 100644 index 0000000000..95af05c55d --- /dev/null +++ b/laws/src/main/scala/cats/laws/BifunctorLaws.scala @@ -0,0 +1,25 @@ +package cats.laws + +import cats.functor.Bifunctor +import cats.syntax.bifunctor._ + +/** + * Laws that must be obeyed by any `Bifunctor`. + */ +trait BifunctorLaws[F[_, _]] { + implicit def F: Bifunctor[F] + + def bifunctorIdentity[A, B](fa: F[A, B]): IsEq[F[A, B]] = + fa.bimap(identity, identity) <-> fa + + def bifunctorComposition[A, B, C, X, Y, Z](fa: F[A, X], f: A => B, f2: B => C, g: X => Y, g2: Y => Z): IsEq[F[C, Z]] = { + fa.bimap(f, g).bimap(f2, g2) <-> fa.bimap(f andThen f2, g andThen g2) + } +} + +object BifunctorLaws { + def apply[F[_,_]](implicit ev: Bifunctor[F]): BifunctorLaws[F] = + new BifunctorLaws[F] { + def F: Bifunctor[F] = ev + } +} diff --git a/laws/src/main/scala/cats/laws/discipline/BifunctorTests.scala b/laws/src/main/scala/cats/laws/discipline/BifunctorTests.scala new file mode 100644 index 0000000000..1f746a1893 --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/BifunctorTests.scala @@ -0,0 +1,36 @@ +package cats.laws.discipline + +import cats.Eq +import cats.functor.Bifunctor +import cats.laws.BifunctorLaws +import org.scalacheck.Arbitrary +import org.scalacheck.Prop._ +import org.typelevel.discipline.Laws + +trait BifunctorTests[F[_, _]] extends Laws { + def laws: BifunctorLaws[F] + + def bifunctor[A, A2, A3, B, B2, B3](implicit + ArbFAB: Arbitrary[F[A, B]], + ArbA2: Arbitrary[A => A2], + ArbA3: Arbitrary[A2 => A3], + ArbB2: Arbitrary[B => B2], + ArbB3: Arbitrary[B2 => B3], + EqFAB: Eq[F[A, B]], + EqFCZ: Eq[F[A3, B3]] + ): RuleSet = { + new DefaultRuleSet( + name = "Bifunctor", + parent = None, + "Bifunctor Identity" -> forAll(laws.bifunctorIdentity[A, B] _), + "Bifunctor associativity" -> forAll(laws.bifunctorComposition[A, A2, A3, B, B2, B3] _) + ) + } +} + +object BifunctorTests { + def apply[F[_, _] : Bifunctor]: BifunctorTests[F] = + new BifunctorTests[F] { + def laws: BifunctorLaws[F] = BifunctorLaws[F] + } +} diff --git a/tests/src/test/scala/cats/tests/IorTests.scala b/tests/src/test/scala/cats/tests/IorTests.scala index 95cc374e71..6a185cdd61 100644 --- a/tests/src/test/scala/cats/tests/IorTests.scala +++ b/tests/src/test/scala/cats/tests/IorTests.scala @@ -2,7 +2,7 @@ package cats package tests import cats.data.{Xor, Ior} -import cats.laws.discipline.{TraverseTests, MonadTests, SerializableTests} +import cats.laws.discipline.{BifunctorTests, TraverseTests, MonadTests, SerializableTests} import cats.laws.discipline.arbitrary._ import org.scalacheck.Arbitrary import org.scalacheck.Arbitrary._ @@ -15,6 +15,7 @@ class IorTests extends CatsSuite { checkAll("Ior[String, Int] with Option", TraverseTests[String Ior ?].traverse[Int, Int, Int, Int, Option, Option]) checkAll("Traverse[String Ior ?]", SerializableTests.serializable(Traverse[String Ior ?])) + checkAll("? Ior ?", BifunctorTests[Ior].bifunctor[Int, Int, Int, String, String, String]) check { forAll { (i: Int Ior String) => diff --git a/tests/src/test/scala/cats/tests/ValidatedTests.scala b/tests/src/test/scala/cats/tests/ValidatedTests.scala index 887bcc303e..cee81aad2b 100644 --- a/tests/src/test/scala/cats/tests/ValidatedTests.scala +++ b/tests/src/test/scala/cats/tests/ValidatedTests.scala @@ -3,7 +3,7 @@ package tests import cats.data.{NonEmptyList, Validated} import cats.data.Validated.{Valid, Invalid} -import cats.laws.discipline.{TraverseTests, ApplicativeTests, SerializableTests} +import cats.laws.discipline.{BifunctorTests, TraverseTests, ApplicativeTests, SerializableTests} import org.scalacheck.{Gen, Arbitrary} import org.scalacheck.Arbitrary._ import org.scalacheck.Prop._ @@ -14,6 +14,7 @@ import scala.util.Try class ValidatedTests extends CatsSuite { checkAll("Validated[String, Int]", ApplicativeTests[Validated[String,?]].applicative[Int, Int, Int]) + checkAll("Validated[?, ?]", BifunctorTests[Validated].bifunctor[Int, Int, Int, Int, Int, Int]) checkAll("Applicative[Validated[String,?]]", SerializableTests.serializable(Applicative[Validated[String,?]])) checkAll("Validated[String, Int] with Option", TraverseTests[Validated[String,?]].traverse[Int, Int, Int, Int, Option, Option]) diff --git a/tests/src/test/scala/cats/tests/XorTTests.scala b/tests/src/test/scala/cats/tests/XorTTests.scala index 1c74a680a6..68261809ae 100644 --- a/tests/src/test/scala/cats/tests/XorTTests.scala +++ b/tests/src/test/scala/cats/tests/XorTTests.scala @@ -2,7 +2,7 @@ package cats.tests import cats.{Id, MonadError} import cats.data.{Xor, XorT} -import cats.laws.discipline.{MonadErrorTests, MonoidKTests, SerializableTests} +import cats.laws.discipline.{BifunctorTests, MonadErrorTests, MonoidKTests, SerializableTests} import cats.laws.discipline.arbitrary._ import org.scalacheck.Prop.forAll @@ -11,6 +11,7 @@ class XorTTests extends CatsSuite { checkAll("XorT[List, String, Int]", MonadErrorTests[XorT[List, ?, ?], String].monadError[Int, Int, Int]) checkAll("XorT[List, String, Int]", MonoidKTests[XorT[List, String, ?]].monoidK[Int]) checkAll("MonadError[XorT[List, ?, ?]]", SerializableTests.serializable(MonadError[XorT[List, ?, ?], String])) + checkAll("XorT[List, ?, ?]", BifunctorTests[XorT[List, ?, ?]].bifunctor[Int, Int, Int, String, String, String]) test("toValidated")(check { forAll { (xort: XorT[List, String, Int]) => diff --git a/tests/src/test/scala/cats/tests/XorTests.scala b/tests/src/test/scala/cats/tests/XorTests.scala index 30d8c86d5b..6d1a86a2f2 100644 --- a/tests/src/test/scala/cats/tests/XorTests.scala +++ b/tests/src/test/scala/cats/tests/XorTests.scala @@ -4,7 +4,7 @@ package tests import cats.data.Xor import cats.data.Xor._ import cats.laws.discipline.arbitrary.xorArbitrary -import cats.laws.discipline.{TraverseTests, MonadErrorTests, SerializableTests} +import cats.laws.discipline.{BifunctorTests, TraverseTests, MonadErrorTests, SerializableTests} import org.scalacheck.{Arbitrary, Gen} import org.scalacheck.Prop._ import org.scalacheck.Prop.BooleanOperators @@ -21,6 +21,7 @@ class XorTests extends CatsSuite { checkAll("Xor[String, Int] with Option", TraverseTests[Xor[String, ?]].traverse[Int, Int, Int, Int, Option, Option]) checkAll("Traverse[Xor[String,?]]", SerializableTests.serializable(Traverse[Xor[String, ?]])) + implicit val arbitraryXor: Arbitrary[Xor[Int, String]] = Arbitrary { for { left <- arbitrary[Boolean] @@ -29,6 +30,8 @@ class XorTests extends CatsSuite { } yield xor } + checkAll("? Xor ?", BifunctorTests[Xor].bifunctor[Int, Int, Int, String, String, String]) + test("fromTryCatch catches matching exceptions") { assert(Xor.fromTryCatch[NumberFormatException]{ "foo".toInt }.isInstanceOf[Xor.Left[NumberFormatException]]) }