From fca2014b53959eedaf245bf483b499868a9b9da6 Mon Sep 17 00:00:00 2001 From: Stephen Lazaro Date: Wed, 13 Dec 2017 14:56:28 -0800 Subject: [PATCH] Add Arrow Choice (#2096) * ArrowChoice, laws, function instance * Kleisli instances, mima exceptions, tests * Update symbols table * Consolidate instances, add ||| * Fix laws * Clean up laws code * Consistency law, match over fold --- build.sbt | 18 +++++ .../main/scala/cats/arrow/ArrowChoice.scala | 43 ++++++++++++ core/src/main/scala/cats/arrow/Choice.scala | 1 + core/src/main/scala/cats/data/Kleisli.scala | 29 +++++--- .../main/scala/cats/instances/function.scala | 16 ++--- core/src/main/scala/cats/syntax/all.scala | 1 + .../main/scala/cats/syntax/arrowChoice.scala | 6 ++ core/src/main/scala/cats/syntax/package.scala | 1 + docs/src/main/tut/faq.md | 56 +++++++-------- .../scala/cats/laws/ArrowChoiceLaws.scala | 50 ++++++++++++++ .../laws/discipline/ArrowChoiceTests.scala | 68 +++++++++++++++++++ .../test/scala/cats/tests/FunctionSuite.scala | 5 +- .../test/scala/cats/tests/KleisliSuite.scala | 8 ++- 13 files changed, 254 insertions(+), 48 deletions(-) create mode 100644 core/src/main/scala/cats/arrow/ArrowChoice.scala create mode 100644 core/src/main/scala/cats/syntax/arrowChoice.scala create mode 100644 laws/src/main/scala/cats/laws/ArrowChoiceLaws.scala create mode 100644 laws/src/main/scala/cats/laws/discipline/ArrowChoiceTests.scala diff --git a/build.sbt b/build.sbt index 1faaf6bc74..4cb7d05429 100644 --- a/build.sbt +++ b/build.sbt @@ -292,6 +292,24 @@ def mimaSettings(moduleName: String) = Seq( exclude[ReversedMissingMethodProblem]("cats.data.CommonIRWSTConstructors.liftK"), exclude[ReversedMissingMethodProblem]("cats.data.KleisliFunctions.liftF"), exclude[ReversedMissingMethodProblem]("cats.data.KleisliFunctions.liftK"), + exclude[IncompatibleResultTypeProblem]("cats.implicits.catsStdInstancesForFunction1"), + exclude[IncompatibleResultTypeProblem]("cats.instances.package#all.catsStdInstancesForFunction1"), + exclude[IncompatibleResultTypeProblem]("cats.instances.Function1Instances.catsStdInstancesForFunction1"), + exclude[ReversedMissingMethodProblem]("cats.instances.Function1Instances.catsStdInstancesForFunction1"), + exclude[ReversedMissingMethodProblem]("cats.instances.Function1Instances.cats$instances$Function1Instances$_setter_$catsStdInstancesForFunction1_="), + exclude[IncompatibleResultTypeProblem]("cats.instances.package#function.catsStdInstancesForFunction1"), + exclude[InheritedNewAbstractMethodProblem]("cats.arrow.ArrowChoice#ToArrowChoiceOps.toArrowChoiceOps"), + exclude[DirectMissingMethodProblem]("cats.data.KleisliInstances.catsDataArrowForKleisli"), + exclude[MissingClassProblem]("cats.data.KleisliArrow"), + exclude[MissingTypesProblem]("cats.data.KleisliCommutativeArrow"), + exclude[InheritedNewAbstractMethodProblem]("cats.arrow.ArrowChoice.choose"), + exclude[InheritedNewAbstractMethodProblem]("cats.arrow.ArrowChoice.choice"), + exclude[InheritedNewAbstractMethodProblem]("cats.arrow.Choice.codiagonal"), + exclude[InheritedNewAbstractMethodProblem]("cats.arrow.Choice.choice"), + exclude[InheritedNewAbstractMethodProblem]("cats.data.KleisliArrowChoice.choose"), + exclude[InheritedNewAbstractMethodProblem]("cats.arrow.ArrowChoice.left"), + exclude[InheritedNewAbstractMethodProblem]("cats.arrow.ArrowChoice.right"), + exclude[ReversedMissingMethodProblem]("cats.arrow.Choice#Ops.|||"), exclude[ReversedMissingMethodProblem]("cats.data.CommonStateTConstructors.liftF"), exclude[ReversedMissingMethodProblem]("cats.data.CommonStateTConstructors.liftK"), exclude[ReversedMissingMethodProblem]("cats.NonEmptyParallel.parForEffect"), diff --git a/core/src/main/scala/cats/arrow/ArrowChoice.scala b/core/src/main/scala/cats/arrow/ArrowChoice.scala new file mode 100644 index 0000000000..c0e7870dbb --- /dev/null +++ b/core/src/main/scala/cats/arrow/ArrowChoice.scala @@ -0,0 +1,43 @@ +package cats +package arrow + +import simulacrum.typeclass + +/** + * Must obey the laws defined in cats.laws.ArrowChoiceLaws. + */ +@typeclass trait ArrowChoice[F[_, _]] extends Arrow[F] with Choice[F] { self => + + /** + * ArrowChoice yields Arrows with choice, allowing distribution + * over coproducts. + * + * Given two `F`s (`f` and `g`), create a new `F` with + * domain the coproduct of the domains of `f` and `g`, + * and codomain the coproduct of the codomains of `f` and `g`. + * This is the sum notion to `split`'s product. + * + * Example: + * {{{ + * scala> import cats.implicits._ + * scala> val toLong: Int => Long = _.toLong + * scala> val toDouble: Float => Double = _.toDouble + * scala> val f: Either[Int, Float] => Either[Long, Double] = toLong +++ toDouble + * scala> f(Left(3)) + * res0: Either[Long,Double] = Left(3) + * scala> f(Right(3)) + * res1: Either[Long,Double] = Right(3.0) + * }}} + */ + @simulacrum.op("+++", alias = true) + def choose[A, B, C, D](f: F[A, C])(g: F[B, D]): F[Either[A, B], Either[C, D]] + + def left[A, B, C](fab: F[A, B]): F[Either[A, C], Either[B, C]] = + choose(fab)(lift(identity[C])) + + def right[A, B, C](fab: F[A, B]): F[Either[C, A], Either[C, B]] = + choose(lift(identity[C]))(fab) + + override def choice[A, B, C](f: F[A, C], g: F[B, C]): F[Either[A, B], C] = + rmap(choose(f)(g))(_.fold(identity, identity)) +} diff --git a/core/src/main/scala/cats/arrow/Choice.scala b/core/src/main/scala/cats/arrow/Choice.scala index 7c1b4ba008..9d94fc87fd 100644 --- a/core/src/main/scala/cats/arrow/Choice.scala +++ b/core/src/main/scala/cats/arrow/Choice.scala @@ -23,6 +23,7 @@ import simulacrum.typeclass * res0: String = false is a boolean * }}} */ + @simulacrum.op("|||", alias = true) def choice[A, B, C](f: F[A, C], g: F[B, C]): F[Either[A, B], C] /** diff --git a/core/src/main/scala/cats/data/Kleisli.scala b/core/src/main/scala/cats/data/Kleisli.scala index ac948af00a..16c69b52df 100644 --- a/core/src/main/scala/cats/data/Kleisli.scala +++ b/core/src/main/scala/cats/data/Kleisli.scala @@ -130,11 +130,10 @@ private[data] sealed abstract class KleisliInstances extends KleisliInstances0 { implicit def F: Monad[F] = F0 } - implicit def catsDataArrowForKleisli[F[_]](implicit M: Monad[F]): Arrow[Kleisli[F, ?, ?]] = - new KleisliArrow[F] { + implicit def catsDataArrowChoiceForKleisli[F[_]](implicit M: Monad[F]): ArrowChoice[Kleisli[F, ?, ?]] = + new KleisliArrowChoice[F] { def F: Monad[F] = M } - } private[data] sealed abstract class KleisliInstances0 extends KleisliInstances1 { @@ -147,12 +146,6 @@ private[data] sealed abstract class KleisliInstances0 extends KleisliInstances1 implicit def catsDataMonadForKleisliId[A]: Monad[Kleisli[Id, A, ?]] = catsDataMonadForKleisli[Id, A] - implicit def catsDataCommutativeArrowForKleisli[F[_]](implicit M: CommutativeMonad[F]): CommutativeArrow[Kleisli[F, ?, ?]] = - new KleisliCommutativeArrow[F] {def F: CommutativeMonad[F] = M } - - implicit val catsDataCommutativeArrowForKleisliId: CommutativeArrow[Kleisli[Id, ?, ?]] = - catsDataCommutativeArrowForKleisli[Id] - implicit def catsDataContravariantMonoidalForKleisli[F[_], A](implicit F0: ContravariantMonoidal[F]): ContravariantMonoidal[Kleisli[F, A, ?]] = new KleisliContravariantMonoidal[F, A] { def F: ContravariantMonoidal[F] = F0 } } @@ -161,6 +154,12 @@ private[data] sealed abstract class KleisliInstances1 extends KleisliInstances2 implicit def catsDataMonadForKleisli[F[_], A](implicit M: Monad[F]): Monad[Kleisli[F, A, ?]] = new KleisliMonad[F, A] { def F: Monad[F] = M } + implicit def catsDataCommutativeArrowForKleisli[F[_]](implicit M: CommutativeMonad[F]): CommutativeArrow[Kleisli[F, ?, ?]] with ArrowChoice[Kleisli[F, ?, ?]] = + new KleisliCommutativeArrow[F] {def F: CommutativeMonad[F] = M } + + implicit val catsDataCommutativeArrowForKleisliId: CommutativeArrow[Kleisli[Id, ?, ?]] = + catsDataCommutativeArrowForKleisli[Id] + implicit def catsDataParallelForKleisli[F[_], M[_], A] (implicit P: Parallel[M, F]): Parallel[Kleisli[M, A, ?], Kleisli[F, A, ?]] = new Parallel[Kleisli[M, A, ?], Kleisli[F, A, ?]]{ implicit val appF = P.applicative @@ -238,11 +237,11 @@ private[data] sealed abstract class KleisliInstances8 { new KleisliFunctor[F, A] { def F: Functor[F] = F0 } } -private[data] trait KleisliCommutativeArrow[F[_]] extends CommutativeArrow[Kleisli[F, ?, ?]] with KleisliArrow[F] { +private[data] trait KleisliCommutativeArrow[F[_]] extends CommutativeArrow[Kleisli[F, ?, ?]] with KleisliArrowChoice[F] { implicit def F: CommutativeMonad[F] } -private[data] trait KleisliArrow[F[_]] extends Arrow[Kleisli[F, ?, ?]] with KleisliCategory[F] with KleisliStrong[F] { +private[data] trait KleisliArrowChoice[F[_]] extends ArrowChoice[Kleisli[F, ?, ?]] with KleisliCategory[F] with KleisliStrong[F] { implicit def F: Monad[F] def lift[A, B](f: A => B): Kleisli[F, A, B] = @@ -250,6 +249,14 @@ private[data] trait KleisliArrow[F[_]] extends Arrow[Kleisli[F, ?, ?]] with Klei override def split[A, B, C, D](f: Kleisli[F, A, B], g: Kleisli[F, C, D]): Kleisli[F, (A, C), (B, D)] = Kleisli{ case (a, c) => F.flatMap(f.run(a))(b => F.map(g.run(c))(d => (b, d))) } + + def choose[A, B, C, D](f: Kleisli[F, A, C])(g: Kleisli[F, B, D]): Kleisli[F, Either[A, B], Either[C, D]] = + Kleisli( + (fe: Either[A, B]) => + fe match { + case Left(a) => F.map(f(a))(Left.apply _) + case Right(b) => F.map(g(b))(Right.apply _) + }) } private[data] trait KleisliStrong[F[_]] extends Strong[Kleisli[F, ?, ?]] { diff --git a/core/src/main/scala/cats/instances/function.scala b/core/src/main/scala/cats/instances/function.scala index 641cb9605e..b711fb37a4 100644 --- a/core/src/main/scala/cats/instances/function.scala +++ b/core/src/main/scala/cats/instances/function.scala @@ -2,7 +2,7 @@ package cats package instances import cats.Contravariant -import cats.arrow.{Category, Choice, CommutativeArrow} +import cats.arrow.{Category, ArrowChoice, CommutativeArrow} import annotation.tailrec @@ -76,13 +76,13 @@ private[instances] sealed trait Function1Instances extends Function1Instances0 { } } - - implicit val catsStdInstancesForFunction1: Choice[Function1] with CommutativeArrow[Function1] = - new Choice[Function1] with CommutativeArrow[Function1] { - def choice[A, B, C](f: A => C, g: B => C): Either[A, B] => C = { - case Left(a) => f(a) - case Right(b) => g(b) - } + implicit val catsStdInstancesForFunction1: ArrowChoice[Function1] with CommutativeArrow[Function1] = + new ArrowChoice[Function1] with CommutativeArrow[Function1] { + def choose[A, B, C, D](f: A => C)(g: B => D): Either[A, B] => Either[C, D] = + _ match { + case Left(a) => Left(f(a)) + case Right(b) => Right(g(b)) + } def lift[A, B](f: A => B): A => B = f diff --git a/core/src/main/scala/cats/syntax/all.scala b/core/src/main/scala/cats/syntax/all.scala index df848fd122..68fe3868fa 100644 --- a/core/src/main/scala/cats/syntax/all.scala +++ b/core/src/main/scala/cats/syntax/all.scala @@ -7,6 +7,7 @@ trait AllSyntax with ApplicativeErrorSyntax with ApplySyntax with ArrowSyntax + with ArrowChoiceSyntax with BifunctorSyntax with BifoldableSyntax with BitraverseSyntax diff --git a/core/src/main/scala/cats/syntax/arrowChoice.scala b/core/src/main/scala/cats/syntax/arrowChoice.scala new file mode 100644 index 0000000000..16adb051e9 --- /dev/null +++ b/core/src/main/scala/cats/syntax/arrowChoice.scala @@ -0,0 +1,6 @@ +package cats +package syntax + +import cats.arrow.ArrowChoice + +trait ArrowChoiceSyntax extends ArrowChoice.ToArrowChoiceOps diff --git a/core/src/main/scala/cats/syntax/package.scala b/core/src/main/scala/cats/syntax/package.scala index 3ad75041af..6ce9e45ff9 100644 --- a/core/src/main/scala/cats/syntax/package.scala +++ b/core/src/main/scala/cats/syntax/package.scala @@ -7,6 +7,7 @@ package object syntax { object applicativeError extends ApplicativeErrorSyntax object apply extends ApplySyntax object arrow extends ArrowSyntax + object arrowChoice extends ArrowChoiceSyntax object bifunctor extends BifunctorSyntax object bifoldable extends BifoldableSyntax object bitraverse extends BitraverseSyntax diff --git a/docs/src/main/tut/faq.md b/docs/src/main/tut/faq.md index b6d41a9e3c..ee009703f8 100644 --- a/docs/src/main/tut/faq.md +++ b/docs/src/main/tut/faq.md @@ -211,33 +211,35 @@ The `~>`, `⊥`, `⊤`, `:<:` and `:≺:` symbols can be imported with `import c All other symbols can be imported with `import cats.implicits._` -| Symbol | Name | Nickname | Type Class | Signature | -| -------------------------------- | -------------------------| ---------------- | ----------------------- | --------------------------------------------------------------------| -| `fa *> fb` | followed by | | `Apply[F[_]]` | `followedBy(fa: F[A])(fb: F[B]): F[B]` | -| `fa <* fb` | for effect | | `Apply[F[_]]` | `forEffect(fa: F[A])(fb: F[B]): F[A]` | -| `x === y` | equals | | `Eq[A]` | `eqv(x: A, y: A): Boolean` | -| `x =!= y` | not equals | | `Eq[A]` | `neqv(x: A, y: A): Boolean` | -| `fa >>= f` | flatMap | | `FlatMap[F[_]]` | `flatMap(fa: F[A])(f: A => F[B]): F[B]` | -| `fa >> fb` | followed by | | `FlatMap[F[_]]` | `followedBy(fa: F[A])(fb: F[B]): F[B]` | -| x |-| y | remove | | `Group[A]` | `remove(x: A, y: A): A` | -| `x > y` | greater than | | `PartialOrder[A]` | `gt(x: A, y: A): Boolean` | -| `x >= y` | greater than or equal | | `PartialOrder[A]` | `gteq(x: A, y: A): Boolean` | -| `x < y` | less than | | `PartialOrder[A]` | `lt(x: A, y: A): Boolean` | -| `x <= y` | less than or equal | | `PartialOrder[A]` | `lteq(x: A, y: A): Boolean` | -| x |+| y | Semigroup combine | | `Semigroup[A]` | `combine(x: A, y: A): A` | -| `x <+> y` | SemigroupK combine | | `SemigroupK[F[_]]` | `combineK(x: F[A], y: F[A]): F[A]` | -| `f <<< g` | Arrow compose | | `Compose[F[_, _]]` | `compose(f: F[B, C], g: F[A, B]): F[A, C]` | -| `f >>> g` | Arrow andThen | | `Compose[F[_, _]]` | `andThen(f: F[B, C], g: F[A, B]): F[A, C]` | -| `f &&& g` | Arrow merge | | `Arrow[F[_, _]]` | `merge[A, B, C](f: F[A, B], g: F[A, C]): F[A, (B, C)]` | -| `f -< g` | Arrow combine and bypass | | `Arrow[F[_, _]]` | `combineAndByPass[A, B, C](f: F[A, B], g: F[B, C]): F[A, (B, C)]` | -| `F ~> G` | natural transformation | | `FunctionK[F[_], G[_]]` | `FunctionK` alias | -| `F :<: G` | injectK | | `InjectK[F[_], G[_]]` | `InjectK` alias | -| `F :≺: G` | injectK | | `InjectK[F[_], G[_]]` | `InjectK` alias | -| `fa &> fb` | parallel followed by | | `Parallel[M[_], F[_]]` | `parFollowedBy[A, B](ma: M[A])(mb: M[B]): M[B]` | -| `fa <& fb` | parallel for effect | | `Parallel[M[_], F[_]]` | `parForEffect[A, B](ma: M[A])(mb: M[B]): M[A]` | -| `⊥` | bottom | | N/A | `Nothing` | -| `⊤` | top | | N/A | `Any` | -| `fa << fb` (Deprecated) | for effect | | `FlatMap[F[_]]` | `forEffect(fa: F[A])(fb: F[B]): F[A]` | +| Symbol | Name | Nickname | Type Class | Signature | +| -------------------------------- | -------------------------| ---------------- | ----------------------- | -----------------------------------------------------------------------------| +| `fa *> fb` | followed by | | `Apply[F[_]]` | `followedBy(fa: F[A])(fb: F[B]): F[B]` | +| `fa <* fb` | for effect | | `Apply[F[_]]` | `forEffect(fa: F[A])(fb: F[B]): F[A]` | +| `x === y` | equals | | `Eq[A]` | `eqv(x: A, y: A): Boolean` | +| `x =!= y` | not equals | | `Eq[A]` | `neqv(x: A, y: A): Boolean` | +| `fa >>= f` | flatMap | | `FlatMap[F[_]]` | `flatMap(fa: F[A])(f: A => F[B]): F[B]` | +| `fa >> fb` | followed by | | `FlatMap[F[_]]` | `followedBy(fa: F[A])(fb: F[B]): F[B]` | +| x |-| y | remove | | `Group[A]` | `remove(x: A, y: A): A` | +| `x > y` | greater than | | `PartialOrder[A]` | `gt(x: A, y: A): Boolean` | +| `x >= y` | greater than or equal | | `PartialOrder[A]` | `gteq(x: A, y: A): Boolean` | +| `x < y` | less than | | `PartialOrder[A]` | `lt(x: A, y: A): Boolean` | +| `x <= y` | less than or equal | | `PartialOrder[A]` | `lteq(x: A, y: A): Boolean` | +| x |+| y | Semigroup combine | | `Semigroup[A]` | `combine(x: A, y: A): A` | +| `x <+> y` | SemigroupK combine | | `SemigroupK[F[_]]` | `combineK(x: F[A], y: F[A]): F[A]` | +| `f <<< g` | Arrow compose | | `Compose[F[_, _]]` | `compose(f: F[B, C], g: F[A, B]): F[A, C]` | +| `f >>> g` | Arrow andThen | | `Compose[F[_, _]]` | `andThen(f: F[B, C], g: F[A, B]): F[A, C]` | +| `f &&& g` | Arrow merge | | `Arrow[F[_, _]]` | `merge[A, B, C](f: F[A, B], g: F[A, C]): F[A, (B, C)]` | +| `f -< g` | Arrow combine and bypass | | `Arrow[F[_, _]]` | `combineAndByPass[A, B, C](f: F[A, B], g: F[B, C]): F[A, (B, C)]` | +| `f +++ g` | ArrowChoice choose | | `ArrowChoice[F[_, _]]` | `choose[A, B, C, D](f: F[A, C])(g: F[B, D]): F[Either [A, B], Either[C, D]]` | +| `f ||| g` | Choice choice | | `Choice[F[_, _]]` | `choice[A, B, C](f: F[A, C], g: F[B, C]): F[Either[A, B], C]` | +| `F ~> G` | natural transformation | | `FunctionK[F[_], G[_]]` | `FunctionK` alias | +| `F :<: G` | injectK | | `InjectK[F[_], G[_]]` | `InjectK` alias | +| `F :≺: G` | injectK | | `InjectK[F[_], G[_]]` | `InjectK` alias | +| `fa &> fb` | parallel followed by | | `Parallel[M[_], F[_]]` | `parFollowedBy[A, B](ma: M[A])(mb: M[B]): M[B]` | +| `fa <& fb` | parallel for effect | | `Parallel[M[_], F[_]]` | `parForEffect[A, B](ma: M[A])(mb: M[B]): M[A]` | +| `⊥` | bottom | | N/A | `Nothing` | +| `⊤` | top | | N/A | `Any` | +| `fa << fb` (Deprecated) | for effect | | `FlatMap[F[_]]` | `forEffect(fa: F[A])(fb: F[B]): F[A]` | ## How can I test instances against their type classes' laws? diff --git a/laws/src/main/scala/cats/laws/ArrowChoiceLaws.scala b/laws/src/main/scala/cats/laws/ArrowChoiceLaws.scala new file mode 100644 index 0000000000..b6b2546e82 --- /dev/null +++ b/laws/src/main/scala/cats/laws/ArrowChoiceLaws.scala @@ -0,0 +1,50 @@ +package cats +package laws + +import cats.arrow.ArrowChoice +import cats.syntax.arrowChoice._ +import cats.syntax.compose._ +import cats.syntax.profunctor._ + +/** + * Laws that must be obeyed by any `cats.arrow.ArrowChoice`. + */ +trait ArrowChoiceLaws[F[_, _]] extends ArrowLaws[F] with ChoiceLaws[F] { + implicit override def F: ArrowChoice[F] + implicit def Function: ArrowChoice[Function1] + + def sumAssoc[A, B, C](e: Either[Either[A, B], C]): Either[A, Either[B, C]] = + e match { + case Left(Left(x)) => Left(x) + case Left(Right(y)) => Right(Left(y)) + case Right(z) => Right(Right(z)) + } + + def leftLiftCommute[A, B, C](f: A => B): IsEq[F[Either[A, C], Either[B, C]]] = + F.left[A, B, C](F.lift[A, B](f)) <-> F.lift[Either[A, C], Either[B, C]](Function.left[A, B, C](f)) + + def leftComposeCommute[A, B, C, D](f: F[A, B], g: F[B, C]): IsEq[F[Either[A, D], Either[C, D]]] = + F.left(f >>> g) <-> (F.left(f) >>> F.left[B, C, D](g)) + + def leftRightConsistent[A, B, C](f: A => B): IsEq[F[Either[C, A], Either[C, B]]] = + F.right[A, B, C](F.lift[A, B](f)) <-> + F.left[A, B, C](F.lift[A, B](f)).dimap((x: Either[C, A]) => x.swap)((y: Either[B, C]) => y.swap) + + def leftAndThenLiftedLeftApplyCommutes[A, B, C](f: F[A, B]): IsEq[F[A, Either[B, C]]] = + (f >>> F.lift[B, Either[B, C]](Left.apply[B, C])) <-> (F.lift[A, Either[A, C]](Left.apply[A, C] _) >>> F.left(f)) + + def leftAndThenRightIdentityCommutes[A, B, C, D](f: F[A, B], g: C => D): IsEq[F[Either[A, C], Either[B, D]]] = + (F.left(f) >>> F.lift(identity[B] _ +++ g)) <-> (F.lift(identity[A] _ +++ g) >>> F.left(f)) + + def leftTwiceCommutesWithSumAssociation[A, B, C, D](f: F[A, D]): IsEq[F[Either[Either[A, B], C], Either[D, Either[B, C]]]] = + (F.left(F.left[A, D, B](f)) >>> F.lift(sumAssoc[D, B, C])) <-> + (F.lift(sumAssoc[A, B, C]) >>> F.left(f)) +} + +object ArrowChoiceLaws { + def apply[F[_, _]](implicit ev: ArrowChoice[F], f: ArrowChoice[Function1]): ArrowChoiceLaws[F] = + new ArrowChoiceLaws[F] { + def F: ArrowChoice[F] = ev + def Function: ArrowChoice[Function1] = f + } +} diff --git a/laws/src/main/scala/cats/laws/discipline/ArrowChoiceTests.scala b/laws/src/main/scala/cats/laws/discipline/ArrowChoiceTests.scala new file mode 100644 index 0000000000..8a2c61f181 --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/ArrowChoiceTests.scala @@ -0,0 +1,68 @@ +package cats +package laws +package discipline + +import cats.arrow.ArrowChoice +import cats.instances.function._ +import org.scalacheck.{Arbitrary, Cogen, Prop} +import Prop._ + +trait ArrowChoiceTests[F[_, _]] extends ArrowTests[F] with ChoiceTests[F] { + def laws: ArrowChoiceLaws[F] + + def arrowChoice[A: Arbitrary, B: Arbitrary, C: Arbitrary, D: Arbitrary, E: Arbitrary, G: Arbitrary](implicit + ArbFAB: Arbitrary[F[A, B]], + ArbFBC: Arbitrary[F[B, C]], + ArbFAC: Arbitrary[F[A, C]], + ArbFAD: Arbitrary[F[A, D]], + ArbFCD: Arbitrary[F[C, D]], + ArbFDE: Arbitrary[F[D, E]], + ArbFEG: Arbitrary[F[E, G]], + CogenA: Cogen[A], + CogenB: Cogen[B], + CogenC: Cogen[C], + CogenD: Cogen[D], + CogenE: Cogen[E], + EqFAA: Eq[F[A, A]], + EqFAB: Eq[F[A, B]], + EqFAC: Eq[F[A, C]], + EqFAD: Eq[F[A, D]], + EqFAG: Eq[F[A, G]], + EqFACB: Eq[F[(A, C), B]], + EqFACBC: Eq[F[(A, C), (B, C)]], + EqFACBD: Eq[F[(A, C), (B, D)]], + EqFADCD: Eq[F[(A, D), (C, D)]], + EqFADCG: Eq[F[(A, D), (C, G)]], + EqFAEDE: Eq[F[(A, E), (D, E)]], + EqFABC: Eq[F[A, (B, C)]], + EqFEAED: Eq[F[(E, A), (E, D)]], + EqFACDBCD: Eq[F[((A, C), D), (B, (C, D))]], + EqFEitherABD: Eq[F[Either[A, B], D]], + EqFEitherCoABC: Eq[F[A, Either[B, C]]], + REqFEitherACD: Eq[F[Either[A, D], Either[C, D]]], + LEqFEitherABC: Eq[F[Either[A, C], Either[B, C]]], + REqFEitherABC: Eq[F[Either[C, A], Either[C, B]]], + REqFEitherABCD: Eq[F[Either[A, C], Either[B, D]]], + EitherAssociationABC: Eq[F[Either[Either[A, B], C], Either[D, Either[B, C]]]] + ): RuleSet = + new RuleSet { + def name: String = "arrowChoice" + def bases: Seq[(String, RuleSet)] = Nil + def parents: Seq[RuleSet] = Seq( + arrow[A, B, C, D, E, G], + choice[A, B, C, D] + ) + def props: Seq[(String, Prop)] = Seq( + "left and lift commute" -> forAll(laws.leftLiftCommute[A, B, C] _), + "left and compose commute" -> forAll(laws.leftComposeCommute[A, B, C, D] _), + "left and right consistent" -> forAll(laws.leftRightConsistent[A, B, C] _), + "left and then lift (Left.apply) commutes" -> forAll(laws.leftAndThenLiftedLeftApplyCommutes[A, B, C] _), + "left and then identity +++ _ commutes" -> forAll(laws.leftAndThenRightIdentityCommutes[A, B, C, D] _), + "left commutes with sum association" -> forAll(laws.leftTwiceCommutesWithSumAssociation[A, B, C, D] _) + ) + } +} +object ArrowChoiceTests { + def apply[F[_, _]: ArrowChoice]: ArrowChoiceTests[F] = + new ArrowChoiceTests[F] { def laws: ArrowChoiceLaws[F] = ArrowChoiceLaws[F] } +} diff --git a/tests/src/test/scala/cats/tests/FunctionSuite.scala b/tests/src/test/scala/cats/tests/FunctionSuite.scala index 278d40d673..38df7b07a7 100644 --- a/tests/src/test/scala/cats/tests/FunctionSuite.scala +++ b/tests/src/test/scala/cats/tests/FunctionSuite.scala @@ -1,7 +1,7 @@ package cats package tests -import cats.arrow.{Choice, CommutativeArrow} +import cats.arrow.{ArrowChoice, Choice, CommutativeArrow} import cats.kernel.laws.HashLaws import cats.kernel.laws.discipline.{ BandTests, @@ -48,6 +48,9 @@ class FunctionSuite extends CatsSuite { checkAll("Function1[Int, Int]", ChoiceTests[Function1].choice[Int, Int, Int, Int]) checkAll("Choice[Function1]", SerializableTests.serializable(Choice[Function1])) + checkAll("Function1[Int, Int]", ArrowChoiceTests[Function1].arrowChoice[Int, Int, Int, Int, Int, Int]) + checkAll("ArrowChoice[Function1]", SerializableTests.serializable(ArrowChoice[Function1])) + checkAll("Function1[Int, Int]", ContravariantTests[? => Int].contravariant[Int, Int, Int]) checkAll("Contravariant[? => Int]", SerializableTests.serializable(Contravariant[? => Int])) diff --git a/tests/src/test/scala/cats/tests/KleisliSuite.scala b/tests/src/test/scala/cats/tests/KleisliSuite.scala index ae21d72cc9..0715cc640f 100644 --- a/tests/src/test/scala/cats/tests/KleisliSuite.scala +++ b/tests/src/test/scala/cats/tests/KleisliSuite.scala @@ -40,11 +40,17 @@ class KleisliSuite extends CatsSuite { checkAll("CommutativeMonad[Kleisli[Option, Int, ?]]",SerializableTests.serializable(CommutativeMonad[Kleisli[Option, Int, ?]])) { - implicit val catsDataArrowForKleisli = Kleisli.catsDataArrowForKleisli[List] + implicit val catsDataArrowForKleisli = Kleisli.catsDataArrowChoiceForKleisli[List] checkAll("Kleisli[List, Int, Int]", ArrowTests[Kleisli[List, ?, ?]].arrow[Int, Int, Int, Int, Int, Int]) checkAll("Arrow[Kleisli[List, ?, ?]]", SerializableTests.serializable(Arrow[Kleisli[List, ?, ?]])) } + { + implicit val catsDataArrowChoiceForKleisli = Kleisli.catsDataArrowChoiceForKleisli[List] + checkAll("Kleisli[List, Int, Int]", ArrowChoiceTests[Kleisli[List, ?, ?]].arrowChoice[Int, Int, Int, Int, Int, Int]) + checkAll("ArrowChoice[Kleisli[List, ?, ?]]", SerializableTests.serializable(ArrowChoice[Kleisli[List, ?, ?]])) + } + { implicit val catsDataCommutativeArrowForKleisli = Kleisli.catsDataCommutativeArrowForKleisli[Option] checkAll("Kleisli[Option, Int, Int]", CommutativeArrowTests[Kleisli[Option, ?, ?]].commutativeArrow[Int, Int, Int, Int, Int, Int])