diff --git a/core/src/main/scala/cats/NotNull.scala b/core/src/main/scala/cats/NotNull.scala new file mode 100644 index 0000000000..60ecf0ca6a --- /dev/null +++ b/core/src/main/scala/cats/NotNull.scala @@ -0,0 +1,26 @@ +package cats + +/** + * An instance of `NotNull[A]` indicates that `A` does not have a static type + * of `Null`. + * + * This can be useful in preventing `Null` from being inferred when a type + * parameter is omitted. + */ +sealed trait NotNull[A] + +object NotNull { + /** + * Since NotNull is just a marker trait with no functionality, it's safe to + * reuse a single instance of it. This helps prevent unnecessary allocations. + */ + private[this] val singleton: NotNull[Any] = new NotNull[Any] {} + + private[this] def ambiguousException: Exception = new Exception("An instance of NotNull[Null] was used. This should never happen. Both ambiguous NotNull[Null] instances should always be in scope if one of them is.") + + implicit def `If you are seeing this, you probably need to add an explicit type parameter somewhere, beause Null is being inferred.`: NotNull[Null] = throw ambiguousException + + implicit def ambiguousNull2: NotNull[Null] = throw ambiguousException + + implicit def notNull[A]: NotNull[A] = singleton.asInstanceOf[NotNull[A]] +} diff --git a/core/src/main/scala/cats/data/Validated.scala b/core/src/main/scala/cats/data/Validated.scala index a7d998f35f..f35f318f01 100644 --- a/core/src/main/scala/cats/data/Validated.scala +++ b/core/src/main/scala/cats/data/Validated.scala @@ -228,22 +228,28 @@ trait ValidatedFunctions { * the resulting `Validated`. Uncaught exceptions are propagated. * * For example: {{{ - * val result: Validated[NumberFormatException, Int] = fromTryCatch[NumberFormatException] { "foo".toInt } + * val result: Validated[NumberFormatException, Int] = catchOnly[NumberFormatException] { "foo".toInt } * }}} */ - def fromTryCatch[T >: Null <: Throwable]: FromTryCatchPartiallyApplied[T] = new FromTryCatchPartiallyApplied[T] + def catchOnly[T >: Null <: Throwable]: CatchOnlyPartiallyApplied[T] = new CatchOnlyPartiallyApplied[T] - final class FromTryCatchPartiallyApplied[T] private[ValidatedFunctions] { - def apply[A](f: => A)(implicit T: ClassTag[T]): Validated[T, A] = { + final class CatchOnlyPartiallyApplied[T] private[ValidatedFunctions] { + def apply[A](f: => A)(implicit T: ClassTag[T], NT: NotNull[T]): Validated[T, A] = try { valid(f) } catch { case t if T.runtimeClass.isInstance(t) => invalid(t.asInstanceOf[T]) } - } } + def catchNonFatal[A](f: => A): Validated[Throwable, A] = + try { + valid(f) + } catch { + case scala.util.control.NonFatal(t) => invalid(t) + } + /** * Converts a `Try[A]` to a `Validated[Throwable, A]`. */ diff --git a/core/src/main/scala/cats/data/Xor.scala b/core/src/main/scala/cats/data/Xor.scala index 4472a08cf4..e0f3cc4766 100644 --- a/core/src/main/scala/cats/data/Xor.scala +++ b/core/src/main/scala/cats/data/Xor.scala @@ -217,20 +217,27 @@ trait XorFunctions { * the resulting `Xor`. Uncaught exceptions are propagated. * * For example: {{{ - * val result: NumberFormatException Xor Int = fromTryCatch[NumberFormatException] { "foo".toInt } + * val result: NumberFormatException Xor Int = catching[NumberFormatException] { "foo".toInt } * }}} */ - def fromTryCatch[T >: Null <: Throwable]: FromTryCatchPartiallyApplied[T] = - new FromTryCatchPartiallyApplied[T] + def catchOnly[T >: Null <: Throwable]: CatchOnlyPartiallyApplied[T] = + new CatchOnlyPartiallyApplied[T] - final class FromTryCatchPartiallyApplied[T] private[XorFunctions] { - def apply[A](f: => A)(implicit T: ClassTag[T]): T Xor A = + final class CatchOnlyPartiallyApplied[T] private[XorFunctions] { + def apply[A](f: => A)(implicit CT: ClassTag[T], NT: NotNull[T]): T Xor A = try { right(f) } catch { - case t if T.runtimeClass.isInstance(t) => + case t if CT.runtimeClass.isInstance(t) => left(t.asInstanceOf[T]) } + } + + def catchNonFatal[A](f: => A): Throwable Xor A = + try { + right(f) + } catch { + case scala.util.control.NonFatal(t) => left(t) } /** diff --git a/docs/src/main/tut/traverse.md b/docs/src/main/tut/traverse.md index 767a4816b2..f4de95ce4a 100644 --- a/docs/src/main/tut/traverse.md +++ b/docs/src/main/tut/traverse.md @@ -93,10 +93,10 @@ import cats.std.list._ import cats.syntax.traverse._ def parseIntXor(s: String): Xor[NumberFormatException, Int] = - Xor.fromTryCatch[NumberFormatException](s.toInt) + Xor.catchOnly[NumberFormatException](s.toInt) def parseIntValidated(s: String): ValidatedNel[NumberFormatException, Int] = - Validated.fromTryCatch[NumberFormatException](s.toInt).toValidatedNel + Validated.catchOnly[NumberFormatException](s.toInt).toValidatedNel val x1 = List("1", "2", "3").traverseU(parseIntXor) val x2 = List("1", "abc", "3").traverseU(parseIntXor) diff --git a/docs/src/main/tut/xor.md b/docs/src/main/tut/xor.md index 358a4fcee4..6c329c7839 100644 --- a/docs/src/main/tut/xor.md +++ b/docs/src/main/tut/xor.md @@ -340,13 +340,20 @@ val xor: Xor[NumberFormatException, Int] = } ``` -However, this can get tedious quickly. `Xor` provides a `fromTryCatch` method on its companion object +However, this can get tedious quickly. `Xor` provides a `catchOnly` method on its companion object that allows you to pass it a function, along with the type of exception you want to catch, and does the above for you. ```tut val xor: Xor[NumberFormatException, Int] = - Xor.fromTryCatch[NumberFormatException]("abc".toInt) + Xor.catchOnly[NumberFormatException]("abc".toInt) +``` + +If you want to catch all (non-fatal) throwables, you can use `catchNonFatal`. + +```tut +val xor: Xor[Throwable, Int] = + Xor.catchNonFatal("abc".toInt) ``` ## Additional syntax diff --git a/tests/src/test/scala/cats/tests/ValidatedTests.scala b/tests/src/test/scala/cats/tests/ValidatedTests.scala index 1e3383201e..3c9ae4d103 100644 --- a/tests/src/test/scala/cats/tests/ValidatedTests.scala +++ b/tests/src/test/scala/cats/tests/ValidatedTests.scala @@ -26,16 +26,21 @@ class ValidatedTests extends CatsSuite { Applicative[Validated[String, ?]].ap2(Invalid("1"), Invalid("2"))(Valid(plus)) should === (Invalid("12")) } - test("fromTryCatch catches matching exceptions") { - assert(Validated.fromTryCatch[NumberFormatException]{ "foo".toInt }.isInstanceOf[Invalid[NumberFormatException]]) + test("catchOnly catches matching exceptions") { + assert(Validated.catchOnly[NumberFormatException]{ "foo".toInt }.isInstanceOf[Invalid[NumberFormatException]]) } - test("fromTryCatch lets non-matching exceptions escape") { + test("catchOnly lets non-matching exceptions escape") { val _ = intercept[NumberFormatException] { - Validated.fromTryCatch[IndexOutOfBoundsException]{ "foo".toInt } + Validated.catchOnly[IndexOutOfBoundsException]{ "foo".toInt } } } + test("catchNonFatal catches non-fatal exceptions") { + assert(Validated.catchNonFatal{ "foo".toInt }.isInvalid) + assert(Validated.catchNonFatal{ throw new Throwable("blargh") }.isInvalid) + } + test("fromTry is invalid for failed try"){ forAll { t: Try[Int] => t.isFailure should === (Validated.fromTry(t).isInvalid) diff --git a/tests/src/test/scala/cats/tests/XorTests.scala b/tests/src/test/scala/cats/tests/XorTests.scala index 369424c3a6..b8f6ebecf2 100644 --- a/tests/src/test/scala/cats/tests/XorTests.scala +++ b/tests/src/test/scala/cats/tests/XorTests.scala @@ -32,16 +32,21 @@ class XorTests extends CatsSuite { checkAll("? Xor ?", BifunctorTests[Xor].bifunctor[Int, Int, Int, String, String, String]) - test("fromTryCatch catches matching exceptions") { - assert(Xor.fromTryCatch[NumberFormatException]{ "foo".toInt }.isInstanceOf[Xor.Left[NumberFormatException]]) + test("catchOnly catches matching exceptions") { + assert(Xor.catchOnly[NumberFormatException]{ "foo".toInt }.isInstanceOf[Xor.Left[NumberFormatException]]) } - test("fromTryCatch lets non-matching exceptions escape") { + test("catchOnly lets non-matching exceptions escape") { val _ = intercept[NumberFormatException] { - Xor.fromTryCatch[IndexOutOfBoundsException]{ "foo".toInt } + Xor.catchOnly[IndexOutOfBoundsException]{ "foo".toInt } } } + test("catchNonFatal catches non-fatal exceptions") { + assert(Xor.catchNonFatal{ "foo".toInt }.isLeft) + assert(Xor.catchNonFatal{ throw new Throwable("blargh") }.isLeft) + } + test("fromTry is left for failed Try") { forAll { t: Try[Int] => t.isFailure should === (Xor.fromTry(t).isLeft)