Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 47 additions & 3 deletions core/src/main/scala/cats/ApplicativeError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@

package cats

import cats.ApplicativeError.CatchOnlyPartiallyApplied
import cats.data.{EitherT, Validated}
import cats.ApplicativeError.{CatchOnlyAsUnsafePartiallyApplied, CatchOnlyPartiallyApplied}
import cats.data.Validated.{Invalid, Valid}
import cats.data.{EitherT, Validated}

import scala.reflect.ClassTag
import scala.util.control.NonFatal
Expand Down Expand Up @@ -264,6 +264,8 @@ trait ApplicativeError[F[_], E] extends Applicative[F] {
/**
* Often E is Throwable. Here we try to call pure or catch
* and raise.
*
* Note: fatal exceptions (as defined by `scala.util.control.NonFatal`) will be propagated
*/
def catchNonFatal[A](a: => A)(implicit ev: Throwable <:< E): F[A] =
try pure(a)
Expand All @@ -274,6 +276,8 @@ trait ApplicativeError[F[_], E] extends Applicative[F] {
/**
* Often E is Throwable. Here we try to call pure or catch
* and raise
*
* Note: fatal exceptions (as defined by `scala.util.control.NonFatal`) will be propagated
*/
def catchNonFatalEval[A](a: Eval[A])(implicit ev: Throwable <:< E): F[A] =
try pure(a.value)
Expand All @@ -282,11 +286,41 @@ trait ApplicativeError[F[_], E] extends Applicative[F] {
}

/**
* Evaluates the specified block, catching exceptions of the specified type. Uncaught exceptions are propagated.
* Evaluates the specified block, catching exceptions of the specified type.
*
* This method is considered unsafe because uncaught exceptions are propagated
* outside of `F`. While the name does not currently reflect this, this may change
* at a future date.
*/
def catchOnly[T >: Null <: Throwable]: CatchOnlyPartiallyApplied[T, F, E] =
new CatchOnlyPartiallyApplied[T, F, E](this)

/**
* Often E can be created from Throwable. Here we try to call pure or
* catch, adapt into E, and raise.
*
* Note: fatal exceptions (as defined by `scala.util.control.NonFatal`) will be propagated
*/
def catchNonFatalAs[A](adapt: Throwable => E)(a: => A): F[A] =
try pure(a)
catch {
case NonFatal(t) => raiseError[A](adapt(t))
}

/**
* Evaluates the specified block, catching exceptions of the specified type.
*
* Caught exceptions are mapped to `E` and raised in `F`.
*
* This method is considered unsafe because uncaught exceptions will be propagated outside of `F`
*
* Note: `catchOnlyAsUnsafe` assumes that, if a specific exception type `T` is expected, there
* exists a mapping from `T` to `E`. If this is not the case, consider either manually
* rethrowing inside the mapping function, or using `catchNonFatalAs`
*/
def catchOnlyAs[T >: Null <: Throwable]: CatchOnlyAsUnsafePartiallyApplied[T, F, E] =
new CatchOnlyAsUnsafePartiallyApplied[T, F, E](this)

/**
* If the error type is Throwable, we can convert from a scala.util.Try
*/
Expand Down Expand Up @@ -383,6 +417,16 @@ object ApplicativeError {
}
}

final private[cats] class CatchOnlyAsUnsafePartiallyApplied[T >: Null <: Throwable, F[_], E](
private val F: ApplicativeError[F, E]
) extends AnyVal {
def apply[A](adapt: T => E)(f: => A)(implicit CT: ClassTag[T], NT: NotNull[T]): F[A] =
try F.pure(f)
catch {
case CT(t) => F.raiseError(adapt(t))
}
}

/**
* lift from scala.Option[A] to a F[A]
*
Expand Down
54 changes: 44 additions & 10 deletions tests/shared/src/test/scala/cats/tests/EitherSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,19 @@

package cats.tests

import cats._
import cats.data.{EitherT, NonEmptyChain, NonEmptyList, NonEmptySet, NonEmptyVector, Validated}
import cats.syntax.bifunctor._
import cats.*
import cats.data.*
import cats.kernel.laws.discipline.{EqTests, MonoidTests, OrderTests, PartialOrderTests, SemigroupTests}
import cats.laws.discipline._
import cats.laws.discipline.arbitrary._
import cats.laws.discipline.*
import cats.laws.discipline.SemigroupalTests.Isomorphisms
import cats.syntax.either._
import cats.laws.discipline.arbitrary.*
import cats.syntax.bifunctor.*
import cats.syntax.either.*
import cats.syntax.eq.*
import cats.syntax.option.*
import org.scalacheck.Prop.*

import scala.util.Try
import cats.syntax.eq._
import org.scalacheck.Prop._

class EitherSuite extends CatsSuite {
implicit val iso: Isomorphisms[Either[Int, *]] = Isomorphisms.invariant[Either[Int, *]]
Expand Down Expand Up @@ -136,6 +137,39 @@ class EitherSuite extends CatsSuite {
assert(Either.catchNonFatal(throw new Throwable("blargh")).isLeft)
}

test("ApplicativeError instance catchNonFatalAs maps exceptions to E") {
val res =
ApplicativeError[Either[String, *], String]
.catchNonFatalAs(_.getMessage)("foo".toInt)
assert(res === Left("For input string: \"foo\""))
}

test("ApplicativeError instance catchNonFatalAs maps exceptions to E") {
val res = ApplicativeError[Either[String, *], String].catchNonFatalAs(_.getMessage)("foo".toInt)
assert(res === Left("For input string: \"foo\""))
}

test("ApplicativeError instance catchOnlyAs maps exceptions of the specified type to E") {
val res =
ApplicativeError[Either[String, *], String]
.catchOnlyAs[NumberFormatException](_.getMessage)("foo".toInt)
assert(res === Left("For input string: \"foo\""))
}

test("ApplicativeError instance catchOnlyAsUnsafe maps exceptions of the specified type to E") {
val res =
ApplicativeError[Either[String, *], String]
.catchOnlyAs[NumberFormatException](_.getMessage)("foo".toInt)
assert(res === Left("For input string: \"foo\""))
}

test("ApplicativeError instance catchOnlyAsUnsafe propagates non-matching exceptions") {
val _ = intercept[NumberFormatException] {
ApplicativeError[Either[String, *], String]
.catchOnlyAs[IndexOutOfBoundsException](_.getMessage)("foo".toInt)
}
}

test("fromTry is left for failed Try") {
forAll { (t: Try[Int]) =>
assert(t.isFailure === (Either.fromTry(t).isLeft))
Expand Down Expand Up @@ -429,8 +463,8 @@ class EitherSuite extends CatsSuite {
final class EitherInstancesSuite extends munit.FunSuite {

test("parallel instance in cats.instances.either") {
import cats.instances.either._
import cats.syntax.parallel._
import cats.instances.either.*
import cats.syntax.parallel.*

def either: Either[String, Int] = Left("Test")
(either, either).parTupled
Expand Down
51 changes: 45 additions & 6 deletions tests/shared/src/test/scala/cats/tests/TrySuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ import cats.{CoflatMap, Eval, Later, Monad, MonadThrow, Semigroupal, Traverse}
import cats.kernel.{Eq, Monoid, Semigroup}
import cats.kernel.laws.discipline.{MonoidTests, SemigroupTests}
import cats.laws.{ApplicativeLaws, CoflatMapLaws, FlatMapLaws, MonadLaws}
import cats.laws.discipline._
import cats.laws.discipline.arbitrary._
import cats.syntax.apply._
import cats.syntax.show._
import cats.laws.discipline.*
import cats.laws.discipline.arbitrary.*
import cats.syntax.either.*
import cats.syntax.apply.*
import cats.syntax.show.*

import scala.util.{Success, Try}
import cats.syntax.eq._
import org.scalacheck.Prop._
import cats.syntax.eq.*
import org.scalacheck.{Arbitrary, Gen}
import org.scalacheck.Prop.*

class TrySuite extends CatsSuite {
implicit val eqThrow: Eq[Throwable] = Eq.allEqual
Expand Down Expand Up @@ -103,6 +106,42 @@ class TrySuite extends CatsSuite {
}
}

test("catchOnlyAs works") {
forAll(Gen.either(Gen.alphaStr, Arbitrary.arbitrary[Int])) { (e: Either[String, Int]) =>
val str = e.fold(identity, _.toString)
val res =
MonadThrow[Try]
.catchOnlyAs[NumberFormatException](new IllegalArgumentException("Bad Number", _))(str.toInt)
.toEither

assertEquals(
res.leftMap { t =>
// Shenanigans because Throwable doesn't have a well-behaved equals
(t.getClass.getCanonicalName, t.getMessage, Option(t.getCause).map(_.getClass.getCanonicalName))
},
e.leftMap { _ =>
(
"java.lang.IllegalArgumentException",
"Bad Number",
Some("java.lang.NumberFormatException")
)
},
clues(res)
)
}
}

test("catchOnlyAs catches only a specified type") {
val res =
Either
.catchNonFatal {
MonadThrow[Try]
.catchOnlyAs[UnsupportedOperationException](new IllegalArgumentException("Bad Number", _))("str".toInt)
.toEither
}
assertEquals(res.leftMap(_.getClass.getCanonicalName), Left("java.lang.NumberFormatException"), clues(res))
}

test("fromTry works") {
forAll { (t: Try[Int]) =>
assert((MonadThrow[Try].fromTry(t)) === t)
Expand Down