diff --git a/core/src/main/scala/cats/ApplicativeError.scala b/core/src/main/scala/cats/ApplicativeError.scala index a64affd5cb..43072acb6d 100644 --- a/core/src/main/scala/cats/ApplicativeError.scala +++ b/core/src/main/scala/cats/ApplicativeError.scala @@ -1,11 +1,12 @@ package cats +import cats.ApplicativeError.CatchOnlyPartiallyApplied import cats.data.{EitherT, Validated} import cats.data.Validated.{Invalid, Valid} import scala.reflect.ClassTag -import scala.util.{Failure, Success, Try} 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. @@ -157,8 +158,6 @@ trait ApplicativeError[F[_], E] extends Applicative[F] { * @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 - * @param map is the function that gets to transform the source - * in case of success */ def redeem[A, B](fa: F[A])(recover: E => B, f: A => B): F[B] = handleError(map(fa)(f))(recover) @@ -216,6 +215,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 +306,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 b1b571b006..3e98c024fe 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 f59ad050a7..5aba43885d 100644 --- a/tests/src/test/scala/cats/tests/TrySuite.scala +++ b/tests/src/test/scala/cats/tests/TrySuite.scala @@ -61,6 +61,23 @@ 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("catchOnly catches only a specified type") { + a[NumberFormatException] should be thrownBy { + MonadError[Try, Throwable].catchOnly[UnsupportedOperationException]("str".toInt) + } + } + test("fromTry works") { forAll { t: Try[Int] => (MonadError[Try, Throwable].fromTry(t)) should ===(t)