diff --git a/core/src/main/scala/cats/syntax/applicativeError.scala b/core/src/main/scala/cats/syntax/applicativeError.scala index cdcabedb6b..353d739b10 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) @@ -86,6 +88,12 @@ final class ApplicativeErrorOps[F[_], E, A](private val fa: F[A]) extends AnyVal def attemptT(implicit F: ApplicativeError[F, E]): EitherT[F, E, A] = F.attemptT(fa) + /** + * Similar to [[attempt]], but it only handles errors of type `EE`. + */ + def attemptNarrow[EE <: E: ClassTag](implicit F: ApplicativeError[F, E]): F[Either[EE, A]] = + F.recover(F.map(fa)(Right[EE, A](_): Either[EE, A])) { case e: EE => Left[EE, A](e) } + def recover(pf: PartialFunction[E, A])(implicit F: ApplicativeError[F, E]): F[A] = F.recover(fa)(pf) diff --git a/tests/src/test/scala/cats/tests/ApplicativeErrorSuite.scala b/tests/src/test/scala/cats/tests/ApplicativeErrorSuite.scala index 658bd97e8d..bc54e4ed52 100644 --- a/tests/src/test/scala/cats/tests/ApplicativeErrorSuite.scala +++ b/tests/src/test/scala/cats/tests/ApplicativeErrorSuite.scala @@ -27,6 +27,43 @@ class ApplicativeErrorSuite extends CatsSuite { failed.attemptT should ===(EitherT[Option, Unit, Int](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("recover syntax transforms an error to a success") { failed.recover { case _ => 7 } should ===(Some(7)) }