Skip to content
182 changes: 182 additions & 0 deletions core/src/main/scala/cats/ErrorControl.scala
Original file line number Diff line number Diff line change
@@ -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))

}

}
16 changes: 16 additions & 0 deletions core/src/main/scala/cats/data/EitherT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 0 additions & 1 deletion core/src/main/scala/cats/data/IndexedStateT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cats
package data

import cats.arrow.{Profunctor, Strong}

import cats.syntax.either._

/**
Expand Down
1 change: 1 addition & 0 deletions core/src/main/scala/cats/data/Kleisli.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
31 changes: 31 additions & 0 deletions core/src/main/scala/cats/data/OptionT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -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 }

Expand Down Expand Up @@ -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]

Expand Down
2 changes: 2 additions & 0 deletions core/src/main/scala/cats/implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ package cats
object implicits
extends syntax.AllSyntax
with syntax.AllSyntaxBinCompat0
with syntax.AllSyntaxBinCompat1
with instances.AllInstances
with instances.AllInstancesBinCompat0
8 changes: 8 additions & 0 deletions core/src/main/scala/cats/instances/all.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package cats
package instances

abstract class AllInstancesBinCompat
extends AllInstances
with AllInstancesBinCompat0

trait AllInstances
extends AnyValInstances
with BigIntInstances
Expand Down Expand Up @@ -32,3 +36,7 @@ trait AllInstances
with TupleInstances
with UUIDInstances
with VectorInstances

trait AllInstancesBinCompat0
extends OptionInstancesExtension
with EitherInstancesExtension
19 changes: 19 additions & 0 deletions core/src/main/scala/cats/instances/either.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
18 changes: 18 additions & 0 deletions core/src/main/scala/cats/instances/option.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion core/src/main/scala/cats/instances/package.scala
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/scala/cats/syntax/all.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package syntax
abstract class AllSyntaxBinCompat
extends AllSyntax
with AllSyntaxBinCompat0
with AllSyntaxBinCompat1

trait AllSyntax
extends AlternativeSyntax
Expand Down Expand Up @@ -57,3 +58,6 @@ trait AllSyntaxBinCompat0
extends UnorderedTraverseSyntax
with ApplicativeErrorExtension
with TrySyntax

trait AllSyntaxBinCompat1
extends ErrorControlSyntax
Loading