cats.ApplicativeError.scala Maven / Gradle / Ivy
The newest version!
package cats
import cats.ApplicativeError.CatchOnlyPartiallyApplied
import cats.data.{EitherT, Validated}
import cats.data.Validated.{Invalid, Valid}
import scala.reflect.ClassTag
import scala.util.control.NonFatal
import scala.util.{Failure, Success, Try}
/**
* An applicative that also allows you to raise and or handle an error value.
*
* This type class allows one to abstract over error-handling applicatives.
*/
trait ApplicativeError[F[_], E] extends Applicative[F] {
/**
* Lift an error into the `F` context.
*
* Example:
* {{{
* scala> import cats.implicits._
*
* // integer-rounded division
* scala> def divide[F[_]](dividend: Int, divisor: Int)(implicit F: ApplicativeError[F, String]): F[Int] =
* | if (divisor === 0) F.raiseError("division by zero")
* | else F.pure(dividend / divisor)
*
* scala> type ErrorOr[A] = Either[String, A]
*
* scala> divide[ErrorOr](6, 3)
* res0: ErrorOr[Int] = Right(2)
*
* scala> divide[ErrorOr](6, 0)
* res1: ErrorOr[Int] = Left(division by zero)
* }}}
*/
def raiseError[A](e: E): F[A]
/**
* Handle any error, potentially recovering from it, by mapping it to an
* `F[A]` value.
*
* @see [[handleError]] to handle any error by simply mapping it to an `A`
* value instead of an `F[A]`.
*
* @see [[recoverWith]] to recover from only certain errors.
*/
def handleErrorWith[A](fa: F[A])(f: E => F[A]): F[A]
/**
* Handle any error, by mapping it to an `A` value.
*
* @see [[handleErrorWith]] to map to an `F[A]` value instead of simply an
* `A` value.
*
* @see [[recover]] to only recover from certain errors.
*/
def handleError[A](fa: F[A])(f: E => A): F[A] = handleErrorWith(fa)(f.andThen(pure))
/**
* Handle errors by turning them into [[scala.util.Either]] values.
*
* If there is no error, then an `scala.util.Right` value will be returned instead.
*
* All non-fatal errors should be handled by this method.
*/
def attempt[A](fa: F[A]): F[Either[E, A]] =
handleErrorWith(
map(fa)(Right(_): Either[E, A])
)(e => pure(Left(e)))
/**
* Similar to [[attempt]], but wraps the result in a [[cats.data.EitherT]] for
* convenience.
*/
def attemptT[A](fa: F[A]): EitherT[F, E, A] = EitherT(attempt(fa))
/**
* Similar to [[attempt]], but it only handles errors of type `EE`.
*/
def attemptNarrow[EE, A](fa: F[A])(implicit tag: ClassTag[EE], ev: EE <:< E): F[Either[EE, A]] =
recover(map(fa)(Right[EE, A](_): Either[EE, A])) { case e: EE => Left[EE, A](e) }
/**
* Recover from certain errors by mapping them to an `A` value.
*
* @see [[handleError]] to handle any/all errors.
*
* @see [[recoverWith]] to recover from certain errors by mapping them to
* `F[A]` values.
*/
def recover[A](fa: F[A])(pf: PartialFunction[E, A]): F[A] =
handleErrorWith(fa)(e => (pf.andThen(pure(_))).applyOrElse(e, raiseError[A](_)))
/**
* Recover from certain errors by mapping them to an `F[A]` value.
*
* @see [[handleErrorWith]] to handle any/all errors.
*
* @see [[recover]] to recover from certain errors by mapping them to `A`
* values.
*/
def recoverWith[A](fa: F[A])(pf: PartialFunction[E, F[A]]): F[A] =
handleErrorWith(fa)(e => pf.applyOrElse(e, raiseError))
/**
* Transform certain errors using `pf` and rethrow them.
* Non matching errors and successful values are not affected by this function.
*
* Example:
* {{{
* scala> import cats._, implicits._
*
* scala> def pf: PartialFunction[String, String] = { case "error" => "ERROR" }
*
* scala> ApplicativeError[Either[String, *], String].adaptError("error".asLeft[Int])(pf)
* res0: Either[String,Int] = Left(ERROR)
*
* scala> ApplicativeError[Either[String, *], String].adaptError("err".asLeft[Int])(pf)
* res1: Either[String,Int] = Left(err)
*
* scala> ApplicativeError[Either[String, *], String].adaptError(1.asRight[String])(pf)
* res2: Either[String,Int] = Right(1)
* }}}
*
* The same function is available in `ApplicativeErrorOps` as `adaptErr` - it cannot have the same
* name because this would result in ambiguous implicits due to `adaptError`
* having originally been included in the `MonadError` API and syntax.
*/
def adaptError[A](fa: F[A])(pf: PartialFunction[E, E]): F[A] =
recoverWith(fa)(pf.andThen(raiseError[A] _))
/**
* Returns a new value that transforms the result of the source,
* given the `recover` or `map` functions, which get executed depending
* on whether the result is successful or if it ends in error.
*
* This is an optimization on usage of [[attempt]] and [[map]],
* this equivalence being available:
*
* {{{
* fa.redeem(fe, fs) <-> fa.attempt.map(_.fold(fe, fs))
* }}}
*
* Usage of `redeem` subsumes [[handleError]] because:
*
* {{{
* fa.redeem(fe, id) <-> fa.handleError(fe)
* }}}
*
* Implementations are free to override it in order to optimize
* error recovery.
*
* @see [[MonadError.redeemWith]], [[attempt]] and [[handleError]]
*
* @param fa is the source whose result is going to get transformed
* @param recover is the function that gets called to recover the source
* in case of error
*/
def redeem[A, B](fa: F[A])(recover: E => B, f: A => B): F[B] =
handleError(map(fa)(f))(recover)
/**
* Execute a callback on certain errors, then rethrow them.
* Any non matching error is rethrown as well.
*
* In the following example, only one of the errors is logged,
* but they are both rethrown, to be possibly handled by another
* layer of the program:
*
* {{{
* scala> import cats._, data._, implicits._
*
* scala> case class Err(msg: String)
*
* scala> type F[A] = EitherT[State[String, *], Err, A]
*
* scala> val action: PartialFunction[Err, F[Unit]] = {
* | case Err("one") => EitherT.liftF(State.set("one"))
* | }
*
* scala> val prog1: F[Int] = (Err("one")).raiseError[F, Int]
* scala> val prog2: F[Int] = (Err("two")).raiseError[F, Int]
*
* scala> prog1.onError(action).value.run("").value
* res0: (String, Either[Err,Int]) = (one,Left(Err(one)))
*
* scala> prog2.onError(action).value.run("").value
* res1: (String, Either[Err,Int]) = ("",Left(Err(two)))
* }}}
*/
def onError[A](fa: F[A])(pf: PartialFunction[E, F[Unit]]): F[A] =
handleErrorWith(fa)(e => (pf.andThen(map2(_, raiseError[A](e))((_, b) => b))).applyOrElse(e, raiseError))
/**
* Often E is Throwable. Here we try to call pure or catch
* and raise.
*/
def catchNonFatal[A](a: => A)(implicit ev: Throwable <:< E): F[A] =
try pure(a)
catch {
case NonFatal(e) => raiseError(e)
}
/**
* Often E is Throwable. Here we try to call pure or catch
* and raise
*/
def catchNonFatalEval[A](a: Eval[A])(implicit ev: Throwable <:< E): F[A] =
try pure(a.value)
catch {
case NonFatal(e) => raiseError(e)
}
/**
* Evaluates the specified block, catching exceptions of the specified type. Uncaught exceptions are propagated.
*/
def catchOnly[T >: Null <: Throwable]: CatchOnlyPartiallyApplied[T, F, E] =
new CatchOnlyPartiallyApplied[T, F, E](this)
/**
* If the error type is Throwable, we can convert from a scala.util.Try
*/
def fromTry[A](t: Try[A])(implicit ev: Throwable <:< E): F[A] =
t match {
case Success(a) => pure(a)
case Failure(e) => raiseError(e)
}
/**
* Convert from scala.Either
*
* Example:
* {{{
* scala> import cats.ApplicativeError
* scala> import cats.instances.option._
*
* scala> ApplicativeError[Option, Unit].fromEither(Right(1))
* res0: scala.Option[Int] = Some(1)
*
* scala> ApplicativeError[Option, Unit].fromEither(Left(()))
* res1: scala.Option[Nothing] = None
* }}}
*/
def fromEither[A](x: E Either A): F[A] =
x match {
case Right(a) => pure(a)
case Left(e) => raiseError(e)
}
/**
* Convert from scala.Option
*
* Example:
* {{{
* scala> import cats.implicits._
* scala> import cats.ApplicativeError
* scala> val F = ApplicativeError[Either[String, *], String]
*
* scala> F.fromOption(Some(1), "Empty")
* res0: scala.Either[String, Int] = Right(1)
*
* scala> F.fromOption(Option.empty[Int], "Empty")
* res1: scala.Either[String, Int] = Left(Empty)
* }}}
*/
def fromOption[A](oa: Option[A], ifEmpty: => E): F[A] =
oa match {
case Some(a) => pure(a)
case None => raiseError(ifEmpty)
}
/**
* Convert from cats.data.Validated
*
* Example:
* {{{
* scala> import cats.implicits._
* scala> import cats.ApplicativeError
*
* scala> ApplicativeError[Option, Unit].fromValidated(1.valid[Unit])
* res0: scala.Option[Int] = Some(1)
*
* scala> ApplicativeError[Option, Unit].fromValidated(().invalid[Int])
* res1: scala.Option[Int] = None
* }}}
*/
def fromValidated[A](x: Validated[E, A]): F[A] =
x match {
case Invalid(e) => raiseError(e)
case Valid(a) => pure(a)
}
}
object ApplicativeError {
def apply[F[_], E](implicit F: ApplicativeError[F, E]): ApplicativeError[F, E] = F
final private[cats] class LiftFromOptionPartially[F[_]](private val dummy: Boolean = true) extends AnyVal {
def apply[E, A](oa: Option[A], ifEmpty: => E)(implicit F: ApplicativeError[F, _ >: E]): F[A] =
oa match {
case Some(a) => F.pure(a)
case None => F.raiseError(ifEmpty)
}
}
final private[cats] class CatchOnlyPartiallyApplied[T, F[_], E](private val F: ApplicativeError[F, E])
extends AnyVal {
def apply[A](f: => A)(implicit CT: ClassTag[T], NT: NotNull[T], ev: Throwable <:< E): F[A] =
try {
F.pure(f)
} catch {
case t if CT.runtimeClass.isInstance(t) =>
F.raiseError(t)
}
}
/**
* lift from scala.Option[A] to a F[A]
*
* Example:
* {{{
* scala> import cats.implicits._
* scala> import cats.ApplicativeError
*
* scala> ApplicativeError.liftFromOption[Either[String, *]](Some(1), "Empty")
* res0: scala.Either[String, Int] = Right(1)
*
* scala> ApplicativeError.liftFromOption[Either[String, *]](Option.empty[Int], "Empty")
* res1: scala.Either[String, Int] = Left(Empty)
* }}}
*/
def liftFromOption[F[_]]: LiftFromOptionPartially[F] = new LiftFromOptionPartially[F]
}