diff --git a/core/src/main/scala/cats/ApplicativeError.scala b/core/src/main/scala/cats/ApplicativeError.scala index 9e72bb9edc..37e391b62c 100644 --- a/core/src/main/scala/cats/ApplicativeError.scala +++ b/core/src/main/scala/cats/ApplicativeError.scala @@ -1,6 +1,8 @@ package cats import cats.data.EitherT + +import scala.reflect.ClassTag import scala.util.{Failure, Success, Try} import scala.util.control.NonFatal @@ -73,6 +75,12 @@ trait ApplicativeError[F[_], E] extends Applicative[F] { */ 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. * diff --git a/core/src/main/scala/cats/syntax/applicativeError.scala b/core/src/main/scala/cats/syntax/applicativeError.scala index 1d9c6ccbb5..cbe7153e1d 100644 --- a/core/src/main/scala/cats/syntax/applicativeError.scala +++ b/core/src/main/scala/cats/syntax/applicativeError.scala @@ -4,6 +4,8 @@ package syntax import cats.data.Validated.{Invalid, Valid} import cats.data.{EitherT, Validated} +import scala.reflect.ClassTag + trait ApplicativeErrorSyntax { implicit final def catsSyntaxApplicativeErrorId[E](e: E): ApplicativeErrorIdOps[E] = new ApplicativeErrorIdOps(e) @@ -83,6 +85,9 @@ final class ApplicativeErrorOps[F[_], E, A](private val fa: F[A]) extends AnyVal def attempt(implicit F: ApplicativeError[F, E]): F[Either[E, A]] = F.attempt(fa) + def attemptNarrow[EE](implicit F: ApplicativeError[F, E], tag: ClassTag[EE], ev: EE <:< E): F[Either[EE, A]] = + F.attemptNarrow[EE, A](fa) + def attemptT(implicit F: ApplicativeError[F, E]): EitherT[F, E, A] = F.attemptT(fa) diff --git a/tests/src/test/scala/cats/tests/ApplicativeErrorSuite.scala b/tests/src/test/scala/cats/tests/ApplicativeErrorSuite.scala index 658bd97e8d..a47e1413d4 100644 --- a/tests/src/test/scala/cats/tests/ApplicativeErrorSuite.scala +++ b/tests/src/test/scala/cats/tests/ApplicativeErrorSuite.scala @@ -23,6 +23,43 @@ class ApplicativeErrorSuite extends CatsSuite { failed.attempt should ===(Option(Left(()))) } + test("attemptNarrow[EE] syntax creates an F[Either[EE, A]]") { + trait Err + case class ErrA() extends Err + case class ErrB() extends Err + + implicit val eqForErr: Eq[Err] = Eq.fromUniversalEquals[Err] + implicit val eqForErrA: Eq[ErrA] = Eq.fromUniversalEquals[ErrA] + implicit val eqForErrB: Eq[ErrB] = Eq.fromUniversalEquals[ErrB] + + val failed: Either[Err, Int] = ErrA().raiseError[Either[Err, ?], Int] + + failed.attemptNarrow[ErrA] should ===(ErrA().asLeft[Int].asRight[Err]) + failed.attemptNarrow[ErrB] should ===(Either.left[Err, Either[ErrB, Int]](ErrA())) + } + + test("attemptNarrow works for parametrized types") { + trait T[A] + case object Str extends T[String] + case class Num(i: Int) extends T[Int] + + implicit def eqForT[A]: Eq[T[A]] = Eq.fromUniversalEquals[T[A]] + implicit val eqForStr: Eq[Str.type] = Eq.fromUniversalEquals[Str.type] + implicit val eqForNum: Eq[Num] = Eq.fromUniversalEquals[Num] + + val e: Either[T[Int], Unit] = Num(1).asLeft[Unit] + e.attemptNarrow[Num] should ===(e.asRight[T[Int]]) + assertTypeError("e.attemptNarrow[Str.type]") + + val e2: Either[T[String], Unit] = Str.asLeft[Unit] + e2.attemptNarrow[Str.type] should ===(e2.asRight[T[String]]) + assertTypeError("e2.attemptNarrow[Num]") + + val e3: Either[List[T[String]], Unit] = List(Str).asLeft[Unit] + e3.attemptNarrow[List[Str.type]] should ===(e3.asRight[List[T[String]]]) + assertTypeError("e3.attemptNarrow[List[Num]]") + } + test("attemptT syntax creates an EitherT") { failed.attemptT should ===(EitherT[Option, Unit, Int](Option(Left(())))) }