Scastie - An interactive playground for Scala.
//> using scala 3
//> using option -Xkind-projector:underscores
def assertEquals[A](observed: A, expected: A) = assert(observed == expected)
// - Boundary / Break -----------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------------
// `boundary` sets a label, `break(a)` goes to that label and returns `a`.
object boundary:
final class Label[-A]
private final case class Break[A](value: A, label: Label[A])
extends RuntimeException(null, null, false, false)
def break[A](value: A)(using l: Label[A]): Nothing = throw Break(value, l)
inline def apply[A](inline body: Label[A] ?=> A): A =
val label = new Label[A]
try body(using label)
catch case Break(value, `label`) => value
// - Results --------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------------
// Kind of like `Either`, but specifically meant for error handling.
enum Result[+E, +A]:
case Success(value: A)
case Failure(error: E)
// Syntactic sugar to lift values in `Result`.
extension [A](value: A)
def success[E]: Result[E, A] = Result.Success(value)
def failure[E]: Result[A, E] = Result.Failure(value)
// - Working with Results --------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------------
// Type of things that can fail.
infix type failsWith[A, E] = boundary.Label[Result[E, Nothing]] ?=> A
object results:
// Forces evaluation `body`, wrapping the result in `Result.Success`.
// We know failures are encoded as `Failure` because of the type of
// the label we're carrying.
inline def apply[E, A](inline body: A failsWith E): Result[E, A] =
boundary:
Result.Success(body)
// Fails with the specified error.
def fail[E](error: E)(using boundary.Label[Result[E, Nothing]]): Nothing =
boundary.break(error.failure)
// De-references the specified result if a `Success`, or fails to the
// enclosing label.
extension [E, A](result: Result[E, A])
def ?(using boundary.Label[Result[E, Nothing]]): A =
result match
case Result.Success(a) => a
case Result.Failure(e) => boundary.break(Result.Failure(e))
// `fail` demonstration: removes the optional layer, failing on the first `None`.
def unwrap[A](as: List[Option[A]]): List[A] failsWith String =
as.map:
case Some(a) => a
case None => fail("found a None")
assertEquals(
observed = results(unwrap(List(Some(1), Some(2)))),
expected = List(1, 2).success
)
assertEquals(
observed = results(unwrap(List(Some(1), Some(2), None))),
expected = "found a None".failure
)
// `?` demonstration: sequence is quite a bit more pleasant implement that way, isn't it?
def sequence[A, E](as: List[Result[E, A]]): Result[E, List[A]] =
results:
as.map(_.?)
assertEquals(
observed = sequence(List(1.success, 2.success)),
expected = List(1, 2).success
)
assertEquals(
observed = sequence(List(1.success, 2.success, "some failure".failure)),
expected = "some failure".failure
)