Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

move catching to either object and make it an either block operator #167

Merged
Merged
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
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
Loading