diff --git a/core/src/main/scala/cats/ErrorControl.scala b/core/src/main/scala/cats/ErrorControl.scala new file mode 100644 index 0000000000..c515e7837a --- /dev/null +++ b/core/src/main/scala/cats/ErrorControl.scala @@ -0,0 +1,182 @@ +package cats + +import cats.data._ + +/** + * A type class for principled error handling. + * `ErrorControl` is designed to be a supplement to `MonadError` with more precise typing. + * It is defined as a relationship between an error-handling type `F[A]` and a non-error-handling type `G[A]`. + * This means a value of `F[A]` is able to produce either a value of `A` or an error of type `E`. + * Unlike `MonadError`'s `handleError` method, the `controlError` function defined in this type class + * will yield a value that free of any errors, since they've all been handled. + * + * Must adhere to the laws defined in cats.laws.ErrorControlLaws. + */ +trait ErrorControl[F[_], G[_], E] extends Serializable { + /** + * The MonadError instance for F[_] + */ + def monadErrorF: MonadError[F, E] + + /** + * The Monad instance for G[_] + */ + def monadG: Monad[G] + + /** + * Handle any error and recover from it, by mapping it to an + * error-free `G[A]` value. + * + * Similar to `handleErrorWith` on `ApplicativeError` + * + * Example: + * {{{ + * scala> import cats._, data._, implicits._ + * + * scala> EitherT(List(42.asRight, "Error!".asLeft, 7.asRight)).controlError(err => List(0, -1)) + * res0: List[Int] = List(42, 0, -1, 7) + * }}} + */ + def controlError[A](fa: F[A])(f: E => G[A]): G[A] + + /** + * Injects this error-free `G[A]` value into an `F[A]`. + */ + def accept[A](ga: G[A]): F[A] + + /** + * Transform this `F[A]` value, by mapping either the error or the valid value to a new error-free `G[B]`. + */ + def control[A, B](fa: F[A])(f: Either[E, A] => G[B]): G[B] = + monadG.flatMap(trial(fa))(f) + + /** + * Handle errors by turning them into [[scala.util.Either]] values inside `G`. + * + * If there is no error, then an `scala.util.Right` value will be returned. + * + * All non-fatal errors should be handled by this method. + * + * Similar to `attempt` on `ApplicativeError`. + */ + def trial[A](fa: F[A]): G[Either[E, A]] = + intercept(monadErrorF.map(fa)(Right(_): Either[E, A]))(Left(_)) + + /** + * Like [[trial]], but returns inside the [[cats.data.EitherT]] monad transformer instead. + */ + def trialT[A](fa: F[A]): EitherT[G, E, A] = + EitherT(trial(fa)) + + /** + * Handle any error and recover from it, by mapping it to `A`. + * + * Similar to `handleError` on `ApplicativeError` + * + * Example: + * {{{ + * scala> import cats._, data._, implicits._ + * + * scala> EitherT(List(42.asRight, "Error!".asLeft, 7.asRight, "Another error".asLeft)).intercept(_ => 0) + * res0: List[Int] = List(42, 0, 7, 0) + * }}} + */ + def intercept[A](fa: F[A])(f: E => A): G[A] = + controlError(fa)(f andThen monadG.pure) + + /** + * The inverse of [[trial]]. + * + * Example: + * {{{ + * scala> import cats._, data._, implicits._ + * + * scala> List(42.asRight, "Error!".asLeft, 7.asRight).absolve[EitherT[List, String, ?]] + * res0: EitherT[List, String, Int] = EitherT(List(Right(42), Left(Error!), Right(7))) + * }}} + */ + def absolve[A](gea: G[Either[E, A]]): F[A] = + monadErrorF.flatMap(accept(gea))(_.fold(monadErrorF.raiseError, monadErrorF.pure)) + + /** + * Turns a successful value into an error specified by the `error` function if it does not satisfy a given predicate. + * + * Example: + * {{{ + * scala> import cats._, data._, implicits._ + * + * scala> List(42, 23, -4).assure[EitherT[List, String, ?]](n => if (n < 0) Some("Negative number: " + n) else None) + * res0: EitherT[List, String, Int] = EitherT(List(Right(42), Right(23), Left(Negative number: -4))) + * }}} + */ + def assure[A](ga: G[A])(error: A => Option[E]): F[A] = + monadErrorF.flatMap(accept(ga))(a => + error(a) match { + case Some(e) => monadErrorF.raiseError(e) + case None => monadErrorF.pure(a) + }) + +} + +object ErrorControl { + + def apply[F[_], G[_], E](implicit ev: ErrorControl[F, G, E]): ErrorControl[F, G, E] = ev + + + implicit def catsErrorControlForStateT[F[_], G[_], S, E] + (implicit E: ErrorControl[F, G, E]): ErrorControl[StateT[F, S, ?], StateT[G, S, ?], E] = + new ErrorControl[StateT[F, S, ?], StateT[G, S, ?], E] { + implicit val F: MonadError[F, E] = E.monadErrorF + implicit val G: Monad[G] = E.monadG + + val monadErrorF: MonadError[StateT[F, S, ?], E] = IndexedStateT.catsDataMonadErrorForIndexedStateT + val monadG: Monad[StateT[G, S, ?]] = IndexedStateT.catsDataMonadForIndexedStateT + + def accept[A](ga: StateT[G, S, A]): StateT[F, S, A] = ga.mapK(new (G ~> F) { + def apply[T](ga: G[T]): F[T] = E.accept(ga) + }) + + def controlError[A](fa: StateT[F, S, A])(f: E => StateT[G, S, A]): StateT[G, S, A] = + IndexedStateT(s => E.controlError(fa.run(s))(e => f(e).run(s))) + + } + + + implicit def catsErrorControlForKleisli[F[_], G[_], R, E] + (implicit E: ErrorControl[F, G, E]): ErrorControl[Kleisli[F, R, ?], Kleisli[G, R, ?], E] = + new ErrorControl[Kleisli[F, R, ?], Kleisli[G, R, ?], E] { + implicit val F: MonadError[F, E] = E.monadErrorF + implicit val G: Monad[G] = E.monadG + + val monadErrorF: MonadError[Kleisli[F, R, ?], E] = Kleisli.catsDataMonadErrorForKleisli + val monadG: Monad[Kleisli[G, R, ?]] = Kleisli.catsDataMonadForKleisli + + def accept[A](ga: Kleisli[G, R, A]): Kleisli[F, R, A] = ga.mapK(new (G ~> F) { + def apply[T](ga: G[T]): F[T] = E.accept(ga) + }) + + def controlError[A](fa: Kleisli[F, R, A])(f: E => Kleisli[G, R, A]): Kleisli[G, R, A] = + Kleisli(r => E.controlError(fa.run(r))(e => f(e).run(r))) + + } + + + implicit def catsErrorControlForWriterT[F[_], G[_], L: Monoid, E] + (implicit M: ErrorControl[F, G, E]): ErrorControl[WriterT[F, L, ?], WriterT[G, L, ?], E] = + new ErrorControl[WriterT[F, L, ?], WriterT[G, L, ?], E] { + implicit val F: MonadError[F, E] = M.monadErrorF + implicit val G: Monad[G] = M.monadG + + val monadErrorF: MonadError[WriterT[F, L, ?], E] = WriterT.catsDataMonadErrorForWriterT + val monadG: Monad[WriterT[G, L, ?]] = WriterT.catsDataMonadForWriterT + + def accept[A](ga: WriterT[G, L, A]): WriterT[F, L, A] = ga.mapK(new (G ~> F) { + def apply[T](ga: G[T]): F[T] = M.accept(ga) + }) + + def controlError[A](fa: WriterT[F, L, A])(f: E => WriterT[G, L, A]): WriterT[G, L, A] = + WriterT(M.controlError(fa.run)(e => f(e).run)) + + } + +} diff --git a/core/src/main/scala/cats/data/EitherT.scala b/core/src/main/scala/cats/data/EitherT.scala index 516034d7d0..fa72c9edab 100644 --- a/core/src/main/scala/cats/data/EitherT.scala +++ b/core/src/main/scala/cats/data/EitherT.scala @@ -465,6 +465,22 @@ private[data] abstract class EitherTInstances extends EitherTInstances1 { val F0: Order[F[Either[L, R]]] = F } + implicit def catsErrorControlForEitherT[F[_]: Monad, E]: ErrorControl[EitherT[F, E, ?], F, E] = + new ErrorControl[EitherT[F, E, ?], F, E] { + val monadErrorF: MonadError[EitherT[F, E, ?], E] = EitherT.catsDataMonadErrorForEitherT + val monadG: Monad[F] = Monad[F] + + def controlError[A](fa: EitherT[F, E, A])(f: E => F[A]): F[A] = + Monad[F].flatMap(fa.value) { + case Left(e) => f(e) + case Right(a) => monadG.pure(a) + } + + def accept[A](ga: F[A]): EitherT[F, E, A] = + EitherT.liftF(ga) + + } + implicit def catsDataShowForEitherT[F[_], L, R](implicit sh: Show[F[Either[L, R]]]): Show[EitherT[F, L, R]] = Contravariant[Show].contramap(sh)(_.value) diff --git a/core/src/main/scala/cats/data/IndexedStateT.scala b/core/src/main/scala/cats/data/IndexedStateT.scala index 92598380af..496a8d2ebd 100644 --- a/core/src/main/scala/cats/data/IndexedStateT.scala +++ b/core/src/main/scala/cats/data/IndexedStateT.scala @@ -2,7 +2,6 @@ package cats package data import cats.arrow.{Profunctor, Strong} - import cats.syntax.either._ /** diff --git a/core/src/main/scala/cats/data/Kleisli.scala b/core/src/main/scala/cats/data/Kleisli.scala index fcdb3e8d3e..81c0687787 100644 --- a/core/src/main/scala/cats/data/Kleisli.scala +++ b/core/src/main/scala/cats/data/Kleisli.scala @@ -162,6 +162,7 @@ private[data] sealed abstract class KleisliInstances extends KleisliInstances0 { new KleisliArrowChoice[F] { def F: Monad[F] = M } + } private[data] sealed abstract class KleisliInstances0 extends KleisliInstances1 { diff --git a/core/src/main/scala/cats/data/OptionT.scala b/core/src/main/scala/cats/data/OptionT.scala index 6b587d0dce..88f985d842 100644 --- a/core/src/main/scala/cats/data/OptionT.scala +++ b/core/src/main/scala/cats/data/OptionT.scala @@ -217,6 +217,22 @@ private[data] sealed abstract class OptionTInstances extends OptionTInstances0 { implicit def catsDataMonadForOptionT[F[_]](implicit F0: Monad[F]): Monad[OptionT[F, ?]] = new OptionTMonad[F] { implicit val F = F0 } + implicit def catsErrorControlForOptionT[F[_]: Monad, E]: ErrorControl[OptionT[F, ?], F, Unit] = + new ErrorControl[OptionT[F, ?], F, Unit] { + val monadErrorF: MonadError[OptionT[F, ?], Unit] = catsDataMonadErrorUnitForOptionT + val monadG: Monad[F] = Monad[F] + + def controlError[A](fa: OptionT[F, A])(f: Unit => F[A]): F[A] = + Monad[F].flatMap(fa.value) { + case Some(a) => monadG.pure(a) + case None => f(()) + } + + def accept[A](ga: F[A]): OptionT[F, A] = + OptionT.liftF(ga) + + } + implicit def catsDataFoldableForOptionT[F[_]](implicit F0: Foldable[F]): Foldable[OptionT[F, ?]] = new OptionTFoldable[F] { implicit val F = F0 } @@ -249,6 +265,9 @@ private[data] sealed abstract class OptionTInstances0 extends OptionTInstances1 private[data] sealed abstract class OptionTInstances1 extends OptionTInstances2 { + implicit def catsDataMonadErrorUnitForOptionT[F[_]](implicit F0: Monad[F]): MonadError[OptionT[F, ?], Unit] = + new OptionTMonadErrorUnit[F] { implicit val F = F0 } + implicit def catsDataMonoidKForOptionT[F[_]](implicit F0: Monad[F]): MonoidK[OptionT[F, ?]] = new OptionTMonoidK[F] { implicit val F = F0 } @@ -297,6 +316,18 @@ private trait OptionTMonadError[F[_], E] extends MonadError[OptionT[F, ?], E] wi OptionT(F.handleErrorWith(fa.value)(f(_).value)) } +private trait OptionTMonadErrorUnit[F[_]] extends MonadError[OptionT[F, ?], Unit] with OptionTMonad[F] { + implicit def F: Monad[F] + + def raiseError[A](e: Unit): OptionT[F, A] = OptionT.none + + def handleErrorWith[A](fa: OptionT[F, A])(f: Unit => OptionT[F, A]): OptionT[F, A] = + OptionT(F.flatMap(fa.value) { + case s @ Some(_) => F.pure(s) + case None => f(()).value + }) +} + private trait OptionTContravariantMonoidal[F[_]] extends ContravariantMonoidal[OptionT[F, ?]] { def F: ContravariantMonoidal[F] diff --git a/core/src/main/scala/cats/implicits.scala b/core/src/main/scala/cats/implicits.scala index 892f7bda86..edc25bf7e8 100644 --- a/core/src/main/scala/cats/implicits.scala +++ b/core/src/main/scala/cats/implicits.scala @@ -3,4 +3,6 @@ package cats object implicits extends syntax.AllSyntax with syntax.AllSyntaxBinCompat0 + with syntax.AllSyntaxBinCompat1 with instances.AllInstances + with instances.AllInstancesBinCompat0 diff --git a/core/src/main/scala/cats/instances/all.scala b/core/src/main/scala/cats/instances/all.scala index 3e08de757b..8516be2af2 100644 --- a/core/src/main/scala/cats/instances/all.scala +++ b/core/src/main/scala/cats/instances/all.scala @@ -1,6 +1,10 @@ package cats package instances +abstract class AllInstancesBinCompat + extends AllInstances + with AllInstancesBinCompat0 + trait AllInstances extends AnyValInstances with BigIntInstances @@ -32,3 +36,7 @@ trait AllInstances with TupleInstances with UUIDInstances with VectorInstances + +trait AllInstancesBinCompat0 + extends OptionInstancesExtension + with EitherInstancesExtension diff --git a/core/src/main/scala/cats/instances/either.scala b/core/src/main/scala/cats/instances/either.scala index c5adfab099..91ae30aa06 100644 --- a/core/src/main/scala/cats/instances/either.scala +++ b/core/src/main/scala/cats/instances/either.scala @@ -3,6 +3,7 @@ package instances import cats.syntax.EitherUtil import cats.syntax.either._ + import scala.annotation.tailrec trait EitherInstances extends cats.kernel.instances.EitherInstances { @@ -155,3 +156,21 @@ trait EitherInstances extends cats.kernel.instances.EitherInstances { } } } + +/** + * Extension to Either instances in a binary compat way + */ +trait EitherInstancesExtension { + implicit def catsErrorControlForEither[E]: ErrorControl[Either[E, ?], Id, E] = + new ErrorControl[Either[E, ?], Id, E] { + val monadErrorF: MonadError[Either[E, ?], E] = cats.instances.either.catsStdInstancesForEither + val monadG: Monad[Id] = cats.catsInstancesForId + + def controlError[A](fa: Either[E, A])(f: E => A): A = fa match { + case Left(e) => f(e) + case Right(a) => a + } + + def accept[A](ga: A): Either[E, A] = Right(ga) + } +} diff --git a/core/src/main/scala/cats/instances/option.scala b/core/src/main/scala/cats/instances/option.scala index 7ca772bc13..4f8639ffa2 100644 --- a/core/src/main/scala/cats/instances/option.scala +++ b/core/src/main/scala/cats/instances/option.scala @@ -121,3 +121,21 @@ trait OptionInstances extends cats.kernel.instances.OptionInstances { } } } + +/** + * Extension to Option instances in a binary compat way + */ +trait OptionInstancesExtension { + implicit val catsStdErrorControlForOption: ErrorControl[Option, Id, Unit] = + new ErrorControl[Option, Id, Unit] { + val monadErrorF: MonadError[Option, Unit] = cats.instances.option.catsStdInstancesForOption + val monadG: Monad[Id] = cats.catsInstancesForId + + def controlError[A](fa: Option[A])(f: Unit => A): A = fa match { + case Some(a) => a + case None => f(()) + } + + def accept[A](ga: A): Option[A] = Some(ga) + } +} diff --git a/core/src/main/scala/cats/instances/package.scala b/core/src/main/scala/cats/instances/package.scala index 1a942e3567..26cba0622c 100644 --- a/core/src/main/scala/cats/instances/package.scala +++ b/core/src/main/scala/cats/instances/package.scala @@ -1,7 +1,7 @@ package cats package object instances { - object all extends AllInstances + object all extends AllInstancesBinCompat object bigInt extends BigIntInstances object bigDecimal extends BigDecimalInstances object bitSet extends BitSetInstances diff --git a/core/src/main/scala/cats/syntax/all.scala b/core/src/main/scala/cats/syntax/all.scala index 2b493b7ae6..a4d0cd5133 100644 --- a/core/src/main/scala/cats/syntax/all.scala +++ b/core/src/main/scala/cats/syntax/all.scala @@ -4,6 +4,7 @@ package syntax abstract class AllSyntaxBinCompat extends AllSyntax with AllSyntaxBinCompat0 + with AllSyntaxBinCompat1 trait AllSyntax extends AlternativeSyntax @@ -57,3 +58,6 @@ trait AllSyntaxBinCompat0 extends UnorderedTraverseSyntax with ApplicativeErrorExtension with TrySyntax + +trait AllSyntaxBinCompat1 + extends ErrorControlSyntax diff --git a/core/src/main/scala/cats/syntax/errorControl.scala b/core/src/main/scala/cats/syntax/errorControl.scala new file mode 100644 index 0000000000..259d6e1893 --- /dev/null +++ b/core/src/main/scala/cats/syntax/errorControl.scala @@ -0,0 +1,53 @@ +package cats +package syntax + +import cats.data.EitherT + + +trait ErrorControlSyntax { + implicit final def catsSyntaxErrorControlF[F[_], E, A] + (fa: F[A])(implicit F: MonadError[F, E]): ErrorControlFOps[F, E, A] = + new ErrorControlFOps[F, E, A](fa) + + implicit final def catsSyntaxErrorControlG[G[_], A] + (ga: G[A])(implicit G: Monad[G]): ErrorControlGOps[G, A] = + new ErrorControlGOps[G, A](ga) + + implicit final def catsSyntaxErrorControlEither[G[_], E, A] + (gea: G[Either[E, A]])(implicit G: Monad[G]): ErrorControlEitherOps[G, E, A] = + new ErrorControlEitherOps[G, E, A](gea) + +} + + +final class ErrorControlFOps[F[_], E, A](val fa: F[A]) extends AnyVal { + + def control[G[_], B](f: Either[E, A] => G[B])(implicit E: ErrorControl[F, G, E]): G[B] = + E.control(fa)(f) + + def controlError[G[_]](f: E => G[A])(implicit E: ErrorControl[F, G, E]): G[A] = + E.controlError(fa)(f) + + def trial[G[_]](implicit E: ErrorControl[F, G, E]): G[Either[E, A]] = + E.trial(fa) + + def trialT[G[_]](implicit E: ErrorControl[F, G, E]): EitherT[G, E, A] = + E.trialT(fa) + + def intercept[G[_]](f: E => A)(implicit E: ErrorControl[F, G, E]): G[A] = + E.intercept(fa)(f) +} + +final class ErrorControlGOps[G[_], A](val ga: G[A]) extends AnyVal { + def assure[F[_]]: AssurePartiallyApplied[F, G, A] = new AssurePartiallyApplied[F, G, A](ga) +} + +final class ErrorControlEitherOps[G[_], E, A](val gea: G[Either[E, A]]) extends AnyVal { + def absolve[F[_]](implicit E: ErrorControl[F, G, E]): F[A] = + E.absolve(gea) +} + +private[syntax] final class AssurePartiallyApplied[F[_], G[_], A](val ga: G[A]) extends AnyVal { + def apply[E](error: A => Option[E])(implicit E: ErrorControl[F, G, E]): F[A] = + E.assure(ga)(error) +} diff --git a/docs/src/main/tut/typeclasses/errorcontrol.md b/docs/src/main/tut/typeclasses/errorcontrol.md new file mode 100644 index 0000000000..ce92f5138e --- /dev/null +++ b/docs/src/main/tut/typeclasses/errorcontrol.md @@ -0,0 +1,50 @@ +--- +layout: docs +title: "ErrorControl" +section: "typeclasses" +source: "core/src/main/scala/cats/ErrorControl.scala" +scaladoc: "#cats.ErrorControl" +--- +# ErrorControl + +`ErrorControl` is a type class for principled error handling. +It is designed to be a supplement to `MonadError` with more precise typing and defines a relationship between an +error-handling type `F[A]` and a non-error-handling type `G[A]`. + +This means a value of `F[A]` is able to produce either a value of `A` or an error of type `E`. +Unlike `MonadError`'s `handleError` method, the `controlError` function defined in this type class +will yield a value that free of any errors, since they've all been handled. + +As an example, because `handleError` takes an `F[A]` and also returns an `F[A]` it's fully legal to chain a bunch of these functions: + +```tut +import cats.implicits._ + +Option(42).handleError(_ => -3).handleError(_ => 67).handleError(_ => 99) +``` + +In an ideal world, our type system should stop us from doing this more than once. +The equivalent of this function, called `intercept`, on `ErrorControl` does fulfill exactly this: + +```scala +trait ErrorControl[F[_], G[_], E] { + ... + + def intercept[A](fa: F[A])(f: E => A): G[A] + +} + +Now, you can only call `intercept` on a value of `F[A]` and it will give you a value of `G[A]`, allowing no further chaining. +Here's an example using the [EitherT](datatypes/eithert.html) monad transformer: + +```tut:book +import cats.data.EitherT + +val fa: EitherT[List, String, Int] = EitherT(List(42.asRight, "Error!".asLeft, 7.asRight, "Another error".asLeft)) + +val correctList: List[Int] = fa.intercept(err => 0) +``` + + + +For more on this topic, check out [this blog post about rethinking MonadError.](https://typelevel.org/blog/2018/04/13/rethinking-monaderror.html) diff --git a/laws/src/main/scala/cats/laws/ErrorControlLaws.scala b/laws/src/main/scala/cats/laws/ErrorControlLaws.scala new file mode 100644 index 0000000000..5dc4b7d1e4 --- /dev/null +++ b/laws/src/main/scala/cats/laws/ErrorControlLaws.scala @@ -0,0 +1,60 @@ +package cats +package laws + +import syntax.flatMap._ +import syntax.apply._ + +/** + * Laws that must be obeyed by any `cats.ErrorControl`. + */ +trait ErrorControlLaws[F[_], G[_], E] { + implicit def E: ErrorControl[F, G, E] + implicit val F: MonadError[F, E] = E.monadErrorF + implicit val G: Monad[G] = E.monadG + + def deriveHandleError[A](fa: F[A], f: E => A): IsEq[F[A]] = + E.accept(E.intercept(fa)(f)) <-> F.handleError(fa)(f) + + def deriveAttempt[A](fa: F[A]): IsEq[F[Either[E, A]]]= + E.accept(E.trial(fa)) <-> F.attempt(fa) + + def deriveEnsureOr[A](ga: G[A], e: A => E, p: A => Boolean): IsEq[F[A]] = { + val f: A => Option[E] = a => if (p(a)) None else Some(e(a)) + F.ensureOr(E.accept(ga))(e)(p) <-> E.assure(ga)(f) + } + + def gNeverHasErrors[A](ga: G[A], f: E => A): IsEq[G[A]] = + E.intercept(E.accept(ga))(f) <-> ga + + def raiseErrorControlError[A](e: E, f: E => G[A]): IsEq[G[A]] = + E.controlError(F.raiseError[A](e))(f) <-> f(e) + + def controlErrorPureIsPure[A](a: A, f: E => G[A]): IsEq[G[A]] = + E.controlError(F.pure(a))(f) <-> G.pure(a) + + def controlConsistency[A](fa: F[A], f: Either[E, A] => G[A]): IsEq[G[A]] = + E.trial(fa).flatMap(f) <-> E.control(fa)(f) + + def raiseErrorIntercept[A](e: E, f: E => A): IsEq[G[A]] = + E.intercept(F.raiseError[A](e))(f) <-> G.pure(f(e)) + + def raiseErrorTrial[A](e: E): IsEq[G[Either[E, A]]] = + E.trial(F.raiseError[A](e)) <-> G.pure(Left(e)) + + def trialAbsolve[A](fa: F[A]): IsEq[F[A]] = + E.absolve(E.trial(fa)) <-> fa + + def applicativeHomomorphism[A](x: G[A], y: G[A]): IsEq[F[A]] = + E.accept(x *> y) <-> E.accept(x) *> E.accept(y) + + def pureHomomorphism[A](a: A): IsEq[F[A]] = + E.accept(G.pure(a)) <-> F.pure(a) + + def flatMapHomomorphism[A](ga: G[A], f: A => G[A]): IsEq[F[A]] = + E.accept(ga.flatMap(f)) <-> E.accept(ga).flatMap(a => E.accept(f(a))) +} + +object ErrorControlLaws { + def apply[F[_], G[_], E](implicit ev: ErrorControl[F, G, E]): ErrorControlLaws[F, G, E] = + new ErrorControlLaws[F, G, E] { def E: ErrorControl[F, G, E] = ev } +} diff --git a/laws/src/main/scala/cats/laws/discipline/ErrorControlTests.scala b/laws/src/main/scala/cats/laws/discipline/ErrorControlTests.scala new file mode 100644 index 0000000000..af851c673f --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/ErrorControlTests.scala @@ -0,0 +1,51 @@ +package cats +package laws +package discipline + +import cats.laws.ErrorControlLaws +import org.scalacheck.Arbitrary +import org.scalacheck.Prop.forAll +import org.typelevel.discipline.Laws + +trait ErrorControlTests[F[_], G[_], E] extends Laws { + def laws: ErrorControlLaws[F, G, E] + + def errorControl[A] + (implicit ArbA: Arbitrary[A], + ArbF: Arbitrary[F[A]], + ArbG: Arbitrary[G[A]], + ArbE: Arbitrary[E], + Arbf: Arbitrary[E => A], + Arbf2: Arbitrary[A => E], + Arbp: Arbitrary[A => Boolean], + ArbfG: Arbitrary[E => G[A]], + ArbfGa: Arbitrary[A => G[A]], + ArbfE: Arbitrary[Either[E, A] => G[A]], + EqFa: Eq[F[A]], + EqGa: Eq[G[A]], + EqFea: Eq[F[Either[E, A]]], + EqGea: Eq[G[Either[E, A]]] + ): RuleSet = + new DefaultRuleSet( + "errorControl", + None, + "raiseError controlError" -> forAll(laws.raiseErrorControlError[A] _), + "raiseError intercept" -> forAll(laws.raiseErrorIntercept[A] _), + "control is trial flatMap" -> forAll(laws.controlConsistency[A] _), + "controlError pure is pure" -> forAll(laws.controlErrorPureIsPure[A] _), + "G never has errors" -> forAll(laws.gNeverHasErrors[A] _), + "raiseError trial" -> forAll(laws.raiseErrorTrial[A] _), + "trial absolve cancel each oter" -> forAll(laws.trialAbsolve[A] _), + "derive attempt" -> forAll(laws.deriveAttempt[A] _), + "derive ensureOr" -> forAll(laws.deriveEnsureOr[A] _), + "derive HandleError" -> forAll(laws.deriveHandleError[A] _), + "applicative homomorphism pure" -> forAll(laws.pureHomomorphism[A] _), + "applicative homomorphism ap" -> forAll(laws.applicativeHomomorphism[A] _), + "monad homomorphism flatMap" -> forAll(laws.flatMapHomomorphism[A] _) + ) +} + +object ErrorControlTests { + def apply[F[_], G[_], E](implicit ev: ErrorControl[F, G, E]): ErrorControlTests[F, G, E] = + new ErrorControlTests[F, G, E] { val laws: ErrorControlLaws[F, G, E] = ErrorControlLaws[F, G, E] } +} diff --git a/testkit/src/main/scala/cats/tests/CatsSuite.scala b/testkit/src/main/scala/cats/tests/CatsSuite.scala index 2caff3e311..92d060c0aa 100644 --- a/testkit/src/main/scala/cats/tests/CatsSuite.scala +++ b/testkit/src/main/scala/cats/tests/CatsSuite.scala @@ -2,11 +2,9 @@ package cats package tests import catalysts.Platform - -import cats.instances.AllInstances -import cats.syntax.{AllSyntax, AllSyntaxBinCompat0, EqOps} - -import org.scalactic.anyvals.{PosZDouble, PosInt, PosZInt} +import cats.instances.{AllInstances, AllInstancesBinCompat0} +import cats.syntax.{AllSyntax, AllSyntaxBinCompat0, AllSyntaxBinCompat1, EqOps} +import org.scalactic.anyvals.{PosInt, PosZDouble, PosZInt} import org.scalatest.{FunSuite, FunSuiteLike, Matchers} import org.scalatest.prop.{Configuration, GeneratorDrivenPropertyChecks} import org.typelevel.discipline.scalatest.Discipline @@ -36,7 +34,10 @@ trait CatsSuite extends FunSuite with Discipline with TestSettings with AllInstances - with AllSyntax with AllSyntaxBinCompat0 + with AllInstancesBinCompat0 + with AllSyntax + with AllSyntaxBinCompat0 + with AllSyntaxBinCompat1 with StrictCatsEquality { self: FunSuiteLike => implicit override val generatorDrivenConfig: PropertyCheckConfiguration = diff --git a/tests/src/test/scala/cats/tests/EitherSuite.scala b/tests/src/test/scala/cats/tests/EitherSuite.scala index 4d3a791ddd..cb8aac893b 100644 --- a/tests/src/test/scala/cats/tests/EitherSuite.scala +++ b/tests/src/test/scala/cats/tests/EitherSuite.scala @@ -32,6 +32,9 @@ class EitherSuite extends CatsSuite { checkAll("Either[ListWrapper[String], Int]", SemigroupTests[Either[ListWrapper[String], Int]].semigroup) checkAll("Semigroup[Either[ListWrapper[String], Int]]", SerializableTests.serializable(Semigroup[Either[ListWrapper[String], Int]])) + checkAll("Either[String, Int]", ErrorControlTests[Either[String, ?], Id, String].errorControl[Int]) + checkAll("ErrorControl[Either[String, ?], Id, String]", SerializableTests.serializable(ErrorControl[Either[String, ?], Id, String])) + val partialOrder = catsStdPartialOrderForEither[Int, String] val order = implicitly[Order[Either[Int, String]]] val monad = implicitly[Monad[Either[Int, ?]]] diff --git a/tests/src/test/scala/cats/tests/EitherTSuite.scala b/tests/src/test/scala/cats/tests/EitherTSuite.scala index b3063bddbf..b5277bdb63 100644 --- a/tests/src/test/scala/cats/tests/EitherTSuite.scala +++ b/tests/src/test/scala/cats/tests/EitherTSuite.scala @@ -59,6 +59,9 @@ class EitherTSuite extends CatsSuite { checkAll("EitherT[ListWrapper, String, Int]", MonadErrorTests[EitherT[ListWrapper, String, ?], String].monadError[Int, Int, Int]) checkAll("MonadError[EitherT[List, ?, ?]]", SerializableTests.serializable(MonadError[EitherT[ListWrapper, String, ?], String])) + checkAll("EitherT[ListWrapper, String, Int]", ErrorControlTests[EitherT[ListWrapper, String, ?], ListWrapper, String].errorControl[Int]) + checkAll("ErrorControl[EitherT[ListWrapper, String, ?], ListWrapper, String]", + SerializableTests.serializable(ErrorControl[EitherT[ListWrapper, String, ?], ListWrapper, String])) } { diff --git a/tests/src/test/scala/cats/tests/IndexedStateTSuite.scala b/tests/src/test/scala/cats/tests/IndexedStateTSuite.scala index 49a1f53e1a..5718d8fe95 100644 --- a/tests/src/test/scala/cats/tests/IndexedStateTSuite.scala +++ b/tests/src/test/scala/cats/tests/IndexedStateTSuite.scala @@ -403,6 +403,12 @@ class IndexedStateTSuite extends CatsSuite { checkAll("StateT[Option, Int, Int]", MonadErrorTests[StateT[Option, Int, ?], Unit].monadError[Int, Int, Int]) checkAll("MonadError[StateT[Option, Int, ?], Unit]", SerializableTests.serializable(MonadError[StateT[Option, Int , ?], Unit])) + + checkAll("StateT[EitherT[Option, String, ?], Int, ?]", + ErrorControlTests[StateT[EitherT[Option, String, ?], Int, ?], StateT[Option, Int, ?], String].errorControl[Int]) + checkAll("ErrorControl[StateT[EitherT[Option, String, ?], Int, ?], StateT[Option, Int, ?], String]", + SerializableTests.serializable(ErrorControl[StateT[EitherT[Option, String, ?], Int, ?], StateT[Option, Int, ?], String])) + } } diff --git a/tests/src/test/scala/cats/tests/KleisliSuite.scala b/tests/src/test/scala/cats/tests/KleisliSuite.scala index 846ca6e4e5..63e1b4fd0e 100644 --- a/tests/src/test/scala/cats/tests/KleisliSuite.scala +++ b/tests/src/test/scala/cats/tests/KleisliSuite.scala @@ -39,13 +39,18 @@ class KleisliSuite extends CatsSuite { checkAll("Semigroupal[Kleisli[Option, Int, ?]]", SerializableTests.serializable(Semigroupal[Kleisli[Option, Int, ?]])) checkAll("Kleisli[(CSemi, ?), Int, ?]", CommutativeFlatMapTests[Kleisli[(CSemi, ?), Int, ?]].commutativeFlatMap[Int, Int, Int]) - checkAll("CommutativeFlatMap[Kleisli[(CSemi, ?), Int, ?]]",SerializableTests.serializable(CommutativeFlatMap[Kleisli[(CSemi, ?), Int, ?]])) + checkAll("CommutativeFlatMap[Kleisli[(CSemi, ?), Int, ?]]", SerializableTests.serializable(CommutativeFlatMap[Kleisli[(CSemi, ?), Int, ?]])) checkAll("Kleisli[Option, Int, ?]", CommutativeMonadTests[Kleisli[Option, Int, ?]].commutativeMonad[Int, Int, Int]) - checkAll("CommutativeMonad[Kleisli[Option, Int, ?]]",SerializableTests.serializable(CommutativeMonad[Kleisli[Option, Int, ?]])) + checkAll("CommutativeMonad[Kleisli[Option, Int, ?]]", SerializableTests.serializable(CommutativeMonad[Kleisli[Option, Int, ?]])) checkAll("Kleisli[Id, Int, ?]", CommutativeMonadTests[Kleisli[Id, Int, ?]].commutativeMonad[Int, Int, Int]) - checkAll("CommutativeMonad[Kleisli[Id, Int, ?]]",SerializableTests.serializable(CommutativeMonad[Kleisli[Id, Int, ?]])) + checkAll("CommutativeMonad[Kleisli[Id, Int, ?]]", SerializableTests.serializable(CommutativeMonad[Kleisli[Id, Int, ?]])) + + checkAll("Kleisli[EitherT[Option, String, ?], Int, ?]", + ErrorControlTests[Kleisli[EitherT[Option, String, ?], Int, ?], Kleisli[Option, Int, ?], String].errorControl[Int]) + checkAll("ErrorControl[Kleisli[EitherT[Option, String, ?], Int, ?], Kleisli[Option, Int, ?], String]", + SerializableTests.serializable(ErrorControl[Kleisli[EitherT[Option, String, ?], Int, ?], Kleisli[Option, Int, ?], String])) { implicit val catsDataArrowForKleisli = Kleisli.catsDataArrowChoiceForKleisli[List] diff --git a/tests/src/test/scala/cats/tests/OptionSuite.scala b/tests/src/test/scala/cats/tests/OptionSuite.scala index 1b88cc6750..3029d1a586 100644 --- a/tests/src/test/scala/cats/tests/OptionSuite.scala +++ b/tests/src/test/scala/cats/tests/OptionSuite.scala @@ -23,7 +23,11 @@ class OptionSuite extends CatsSuite { checkAll("Option with Unit", MonadErrorTests[Option, Unit].monadError[Int, Int, Int]) checkAll("MonadError[Option, Unit]", SerializableTests.serializable(MonadError[Option, Unit])) - test("show") { + checkAll("Option[Int]", ErrorControlTests[Option, Id, Unit].errorControl[Int]) + checkAll("ErrorControl[Option, Id, Unit]", SerializableTests.serializable(ErrorControl[Option, Id, Unit])) + + + test("show") { none[Int].show should === ("None") 1.some.show should === ("Some(1)") diff --git a/tests/src/test/scala/cats/tests/OptionTSuite.scala b/tests/src/test/scala/cats/tests/OptionTSuite.scala index 32e3458fda..7f253e374c 100644 --- a/tests/src/test/scala/cats/tests/OptionTSuite.scala +++ b/tests/src/test/scala/cats/tests/OptionTSuite.scala @@ -2,7 +2,7 @@ package cats package tests import cats.data.{Const, OptionT} -import cats.kernel.laws.discipline.{MonoidTests, SemigroupTests, OrderTests, PartialOrderTests, EqTests} +import cats.kernel.laws.discipline.{EqTests, MonoidTests, OrderTests, PartialOrderTests, SemigroupTests} import cats.laws.discipline._ import cats.laws.discipline.arbitrary._ @@ -66,6 +66,13 @@ class OptionTSuite extends CatsSuite { checkAll("OptionT[ListWrapper, Int]", MonoidKTests[OptionT[ListWrapper, ?]].monoidK[Int]) checkAll("MonoidK[OptionT[ListWrapper, ?]]", SerializableTests.serializable(MonoidK[OptionT[ListWrapper, ?]])) + checkAll("OptionT[ListWrapper, Int]", MonadErrorTests[OptionT[ListWrapper, ?], Unit].monadError[Int, Int, Int]) + checkAll("MonadError[OptionT[List, ?]]", SerializableTests.serializable(MonadError[OptionT[ListWrapper, ?], Unit])) + + checkAll("OptionT[ListWrapper, Int]", ErrorControlTests[OptionT[ListWrapper, ?], ListWrapper, Unit].errorControl[Int]) + checkAll("ErrorControl[OptionT[ListWrapper, ?], ListWrapper, Unit]", + SerializableTests.serializable(ErrorControl[OptionT[ListWrapper, ?], ListWrapper, Unit])) + Monad[OptionT[ListWrapper, ?]] FlatMap[OptionT[ListWrapper, ?]] Applicative[OptionT[ListWrapper, ?]] diff --git a/tests/src/test/scala/cats/tests/SyntaxSuite.scala b/tests/src/test/scala/cats/tests/SyntaxSuite.scala index 6bcd3b987b..9386dc4cb3 100644 --- a/tests/src/test/scala/cats/tests/SyntaxSuite.scala +++ b/tests/src/test/scala/cats/tests/SyntaxSuite.scala @@ -3,7 +3,7 @@ package tests import cats.arrow.Compose import cats.instances.AllInstances -import cats.syntax.AllSyntax +import cats.syntax.AllSyntaxBinCompat /** @@ -24,7 +24,7 @@ import cats.syntax.AllSyntax * * None of these tests should ever run, or do any runtime checks. */ -object SyntaxSuite extends AllInstances with AllSyntax { +object SyntaxSuite extends AllSyntaxBinCompat with AllInstances { // pretend we have a value of type A def mock[A]: A = ??? @@ -162,6 +162,25 @@ object SyntaxSuite extends AllInstances with AllSyntax { val gunit: G[F[A]] = fga.nonEmptySequence } + def testErrorControl[F[_], G[_]: Monad, E, A, B](implicit E: ErrorControl[F, G, E], M: MonadError[F, E]): Unit = { + val fa = mock[F[A]] + val f = mock[E => G[A]] + val ga = fa.controlError(f) + + val f2 = mock[E => A] + val ga2 = fa.intercept(f2) + + val gea = fa.trial + val et = fa.trialT + + val f3 = mock[Either[E, A] => G[B]] + val gb = fa.control(f3) + + val fa2 = gea.absolve[F] + + val f4 = mock[A => Option[E]] + val fa3 = ga.assure[F](f4) + } def testParallel[M[_]: Monad, F[_], T[_]: Traverse, A, B](implicit P: Parallel[M, F]): Unit = { diff --git a/tests/src/test/scala/cats/tests/WriterTSuite.scala b/tests/src/test/scala/cats/tests/WriterTSuite.scala index 19b98868ab..ef61021300 100644 --- a/tests/src/test/scala/cats/tests/WriterTSuite.scala +++ b/tests/src/test/scala/cats/tests/WriterTSuite.scala @@ -24,6 +24,11 @@ class WriterTSuite extends CatsSuite { checkAll("WriterT[Show, Int, Int]", ContravariantTests[WriterT[Show, Int, ?]].contravariant[Int, Int, Int]) checkAll("Contravariant[WriterT[Show, Int, Int]]", SerializableTests.serializable(Contravariant[WriterT[Show, Int, ?]])) + checkAll("WriterT[EitherT[Option, String, ?], Int, ?]", + ErrorControlTests[WriterT[EitherT[Option, String, ?], Int, ?], WriterT[Option, Int, ?], String].errorControl[Int]) + checkAll("ErrorControl[WriterT[EitherT[Option, String, ?], Int, ?], WriterT[Option, Int, ?], String]", + SerializableTests.serializable(ErrorControl[WriterT[EitherT[Option, String, ?], Int, ?], WriterT[Option, Int, ?], String])) + // check that this resolves Eq[Writer[Int, Int]]