diff --git a/core/src/main/scala/cats/ApplicativeError.scala b/core/src/main/scala/cats/ApplicativeError.scala index a64affd5cb6..120ad915581 100644 --- a/core/src/main/scala/cats/ApplicativeError.scala +++ b/core/src/main/scala/cats/ApplicativeError.scala @@ -2,6 +2,8 @@ package cats import cats.data.{EitherT, Validated} import cats.data.Validated.{Invalid, Valid} +import cats.ApplicativeError.CatchOnlyPartiallyApplied +import cats.data.EitherT import scala.reflect.ClassTag import scala.util.{Failure, Success, Try} @@ -91,7 +93,7 @@ trait ApplicativeError[F[_], E] extends Applicative[F] { * `F[A]` values. */ def recover[A](fa: F[A])(pf: PartialFunction[E, A]): F[A] = - handleErrorWith(fa)(e => (pf.andThen(pure _)).applyOrElse(e, raiseError _)) + handleErrorWith(fa)(e => pf.andThen(pure).applyOrElse(e, raiseError)) /** * Recover from certain errors by mapping them to an `F[A]` value. @@ -129,7 +131,7 @@ trait ApplicativeError[F[_], E] extends Applicative[F] { * 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] _)) + recoverWith(fa)(pf.andThen(raiseError[A])) /** * Returns a new value that transforms the result of the source, @@ -194,7 +196,7 @@ trait ApplicativeError[F[_], E] extends Applicative[F] { * }}} */ 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)) + 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 @@ -216,6 +218,12 @@ trait ApplicativeError[F[_], E] extends Applicative[F] { 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 */ @@ -301,6 +309,17 @@ object ApplicativeError { } } + 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] * diff --git a/core/src/main/scala/cats/instances/try.scala b/core/src/main/scala/cats/instances/try.scala index b1b571b0066..3e98c024fe1 100644 --- a/core/src/main/scala/cats/instances/try.scala +++ b/core/src/main/scala/cats/instances/try.scala @@ -137,6 +137,10 @@ trait TryInstances extends TryInstances1 { } override def isEmpty[A](fa: Try[A]): Boolean = fa.isFailure + + override def catchNonFatal[A](a: => A)(implicit ev: Throwable <:< Throwable): Try[A] = Try(a) + + override def catchNonFatalEval[A](a: Eval[A])(implicit ev: Throwable <:< Throwable): Try[A] = Try(a.value) } // scalastyle:on method.length diff --git a/tests/src/test/scala/cats/tests/TrySuite.scala b/tests/src/test/scala/cats/tests/TrySuite.scala index f59ad050a70..ef3d698c55e 100644 --- a/tests/src/test/scala/cats/tests/TrySuite.scala +++ b/tests/src/test/scala/cats/tests/TrySuite.scala @@ -61,6 +61,17 @@ class TrySuite extends CatsSuite { res should not be (null) } } + + test("catchOnly works") { + forAll { e: Either[String, Int] => + val str = e.fold(identity, _.toString) + val res = MonadError[Try, Throwable].catchOnly[NumberFormatException](str.toInt) + // the above should just never cause an uncaught exception + // this is a somewhat bogus test: + res should not be (null) + } + } + test("fromTry works") { forAll { t: Try[Int] => (MonadError[Try, Throwable].fromTry(t)) should ===(t)