Skip to content

Commit

Permalink
move catching to either object and make it an either block operator (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
lbialy authored Jul 1, 2024
1 parent a6c115a commit 6771b49
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 13 deletions.
47 changes: 42 additions & 5 deletions core/src/main/scala/ox/either.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import scala.util.control.NonFatal

object either:

/** Catches non-fatal exceptions that occur when evaluating `t` and returns them as the left side of the returned `Either`. */
inline def catching[T](inline t: Label[Either[Throwable, T]] ?=> T): Either[Throwable, T] =
try boundary(Right(t))
catch case NonFatal(e) => Left(e)

private type NotNested = NotGiven[Label[Either[Nothing, Nothing]]]

/** Within an [[either]] block, allows unwrapping [[Either]] and [[Option]] values using [[ok()]]. The result is the right-value of an
Expand Down Expand Up @@ -55,6 +60,24 @@ object either:
case _ => error("`.ok()` can only be used within an `either` call.\nIs it present?")
}

/** Specialized extensions for Right & Left are necessary to prevent compile-time warning about unreachable cases in inlined pattern
* matches when call site has a specific type.
*/
extension [E, A](inline t: Right[E, A])
/** Unwrap the value of the `Either`, returning value of type `A` on guaranteed `Right` case. */
transparent inline def ok(): A = t.value

extension [E, A](inline t: Left[E, A])
/** Unwrap the value of the `Either`, short-circuiting the computation to the enclosing [[either]]. */
transparent inline def ok(): A =
summonFrom {
case given boundary.Label[Either[E, Nothing]] =>
break(t.asInstanceOf[Either[E, Nothing]])
case given boundary.Label[Either[Nothing, Nothing]] =>
error("The enclosing `either` call uses a different error type.\nIf it's explicitly typed, is the error type correct?")
case _ => error("`.ok()` can only be used within an `either` call.\nIs it present?")
}

extension [A](inline t: Option[A])
/** Unwrap the value of the `Option`, short-circuiting the computation to the enclosing [[either]], in case this is a `None`. */
transparent inline def ok(): A =
Expand All @@ -70,6 +93,25 @@ object either:
case _ => error("`.ok()` can only be used within an `either` call.\nIs it present?")
}

/** Specialized extensions for Some & None are necessary to prevent compile-time warning about unreachable cases in inlined pattern
* matches when call site has a specific type.
*/
extension [A](inline t: Some[A])
/** Unwrap the value of the `Option`, returning value of type `A` on guaranteed `Some` case. */
transparent inline def ok(): A = t.value

extension [A](inline t: None.type)
/** Unwrap the value of the `Option`, short-circuiting the computation to the enclosing [[either]] on guaranteed `None`. */
transparent inline def ok(): A =
summonFrom {
case given boundary.Label[Either[Unit, Nothing]] => break(Left(()))
case given boundary.Label[Either[Nothing, Nothing]] =>
error(
"The enclosing `either` call uses a different error type.\nIf it's explicitly typed, is the error type correct?\nNote that for options, the error type must contain a `Unit`."
)
case _ => error("`.ok()` can only be used within an `either` call.\nIs it present?")
}

extension [E, A](inline f: Fork[Either[E, A]])
/** Join the fork and unwrap the value of its `Either` result, short-circuiting the computation to the enclosing [[either]], in case
* this is a left-value.
Expand All @@ -86,8 +128,3 @@ object either:
case given boundary.Label[Either[Nothing, Nothing]] =>
error("The enclosing `either` call uses a different error type.\nIf it's explicitly typed, is the error type correct?")
}

/** Catches non-fatal exceptions that occur when evaluating `t` and returns them as the left side of the returned `Either`. */
inline def catching[T](inline t: => T): Either[Throwable, T] =
try Right(t)
catch case NonFatal(e) => Left(e)
18 changes: 16 additions & 2 deletions core/src/test/scala/ox/EitherTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import ox.either.{fail, ok}

import scala.util.boundary.Label

case class ComparableException(msg: String) extends Exception(msg)

class EitherTest extends AnyFlatSpec with Matchers:
val ok1: Either[Int, String] = Right("x")
val ok2: Either[Int, String] = Right("y")
Expand Down Expand Up @@ -115,15 +117,27 @@ class EitherTest extends AnyFlatSpec with Matchers:
}

it should "catch exceptions" in {
catching(throw new RuntimeException("boom")).left.map(_.getMessage) shouldBe Left("boom")
either.catching(throw new RuntimeException("boom")).left.map(_.getMessage) shouldBe Left("boom")
}

it should "not catch fatal exceptions" in {
val e = intercept[InterruptedException](catching(throw new InterruptedException()))
val e = intercept[InterruptedException](either.catching(throw new InterruptedException()))

e shouldBe a[InterruptedException]
}

it should "provide an either scope when catching" in {
val val1: Either[Throwable, Int] = Left(ComparableException("oh no"))

either.catching(val1.ok()) shouldBe Left(ComparableException("oh no"))
}

it should "report a proper compilation error when wrong error type is used for ok() in catching block" in {
val e = intercept[TestFailedException](assertCompiles("""either.catching(fail1.ok())"""))

e.getMessage should include("The enclosing `either` call uses a different error type.")
}

it should "work when combined with mapPar" in {
def intToEither(i: Int): Either[String, Int] =
if i % 2 == 0 then Right(i) else Left(s"$i is odd")
Expand Down
4 changes: 2 additions & 2 deletions doc/basics/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ Exception-throwing code can be converted to an `Either` using `catching`. Note t
exceptions!

```scala mdoc:compile-only
import ox.catching
import ox.either

val result: Either[Throwable, String] = catching(throw new RuntimeException("boom"))
val result: Either[Throwable, String] = either.catching(throw new RuntimeException("boom"))
```

### Nested `either` blocks
Expand Down
2 changes: 1 addition & 1 deletion doc/basics/quick-example.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ val result1: (Int, String) = par(computation1, computation2)

// timeout a computation
def computation: Int = { sleep(2.seconds); 1 }
val result2: Either[Throwable, Int] = catching(timeout(1.second)(computation))
val result2: Either[Throwable, Int] = either.catching(timeout(1.second)(computation))

// structured concurrency & supervision
supervised {
Expand Down
4 changes: 2 additions & 2 deletions generated-doc/out/basics/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ Exception-throwing code can be converted to an `Either` using `catching`. Note t
exceptions!

```scala
import ox.catching
import ox.either

val result: Either[Throwable, String] = catching(throw new RuntimeException("boom"))
val result: Either[Throwable, String] = either.catching(throw new RuntimeException("boom"))
```

### Nested `either` blocks
Expand Down
2 changes: 1 addition & 1 deletion generated-doc/out/basics/quick-example.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ val result1: (Int, String) = par(computation1, computation2)

// timeout a computation
def computation: Int = { sleep(2.seconds); 1 }
val result2: Either[Throwable, Int] = catching(timeout(1.second)(computation))
val result2: Either[Throwable, Int] = either.catching(timeout(1.second)(computation))

// structured concurrency & supervision
supervised {
Expand Down

0 comments on commit 6771b49

Please sign in to comment.