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

Introduce Bifunctor Laws #559

Merged
merged 2 commits into from
Oct 4, 2015
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions core/src/main/scala/cats/data/Ior.scala
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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 bifunctor: Bifunctor[Ior] =
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, this is super nitpicky, but could you name this iorBifunctor just to match the convention of the instances above it?

Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly for the instances in the other files.

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)
Copy link
Member

Choose a reason for hiding this comment

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

Could you also remove the parens from f and g? If functions have only one parameter we usually omit them.

}
}

sealed abstract class IorInstances0 {
Expand Down
11 changes: 9 additions & 2 deletions core/src/main/scala/cats/data/Validated.scala
Original file line number Diff line number Diff line change
@@ -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 {

Expand Down Expand Up @@ -172,6 +174,11 @@ sealed abstract class ValidatedInstances extends ValidatedInstances1 {
def show(f: Validated[A,B]): String = f.show
}

implicit def bifunctor: 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]] =
Expand Down
8 changes: 8 additions & 0 deletions core/src/main/scala/cats/data/Xor.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cats
package data

import cats.functor.Bifunctor

import scala.reflect.ClassTag
import scala.util.{Failure, Success, Try}

Expand Down Expand Up @@ -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 bifunctor: 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)
Expand Down
9 changes: 9 additions & 0 deletions core/src/main/scala/cats/data/XorT.scala
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down Expand Up @@ -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 bifunctor[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 {
Expand Down
25 changes: 25 additions & 0 deletions laws/src/main/scala/cats/laws/BifunctorLaws.scala
Original file line number Diff line number Diff line change
@@ -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
}
}
36 changes: 36 additions & 0 deletions laws/src/main/scala/cats/laws/discipline/BifunctorTests.scala
Original file line number Diff line number Diff line change
@@ -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]
}
}
3 changes: 2 additions & 1 deletion tests/src/test/scala/cats/tests/IorTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand All @@ -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) =>
Expand Down
3 changes: 2 additions & 1 deletion tests/src/test/scala/cats/tests/ValidatedTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand All @@ -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])
Expand Down
3 changes: 2 additions & 1 deletion tests/src/test/scala/cats/tests/XorTTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]) =>
Expand Down
5 changes: 4 additions & 1 deletion tests/src/test/scala/cats/tests/XorTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand All @@ -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]])
}
Expand Down