diff --git a/tests/run/errorhandling.check b/tests/run/errorhandling.check new file mode 100644 index 000000000000..882ca57f5022 --- /dev/null +++ b/tests/run/errorhandling.check @@ -0,0 +1,4 @@ +breakTest +optTest +resultTest +Person(Kostas,5) diff --git a/tests/run/errorhandling/Result.scala b/tests/run/errorhandling/Result.scala new file mode 100644 index 000000000000..23f42c8100ac --- /dev/null +++ b/tests/run/errorhandling/Result.scala @@ -0,0 +1,70 @@ +package dotty.util +import boundary.Label + +abstract class Result[+T, +E] +case class Ok[+T](value: T) extends Result[T, Nothing] +case class Err[+E](value: E) extends Result[Nothing, E] + +object Result: + extension [T, E](r: Result[T, E]) + + /** `_.?` propagates Err to current Label */ + transparent inline def ? (using Label[Err[E]]): T = r match + case r: Ok[_] => r.value + case err => break(err.asInstanceOf[Err[E]]) + + /** If this is an `Err`, map its value */ + def mapErr[E1](f: E => E1): Result[T, E1] = r match + case err: Err[_] => Err(f(err.value)) + case ok: Ok[_] => ok + + /** Map Ok values, propagate Errs */ + def map[U](f: T => U): Result[U, E] = r match + case Ok(x) => Ok(f(x)) + case err: Err[_] => err + + /** Flatmap Ok values, propagate Errs */ + def flatMap[U](f: T => Result[U, E]): Result[U, E] = r match + case Ok(x) => f(x) + case err: Err[_] => err + + /** Validate both `r` and `other`; return a pair of successes or a list of failures. */ + def * [U](other: Result[U, E]): Result[(T, U), List[E]] = (r, other) match + case (Ok(x), Ok(y)) => Ok((x, y)) + case (Ok(_), Err(e)) => Err(e :: Nil) + case (Err(e), Ok(_)) => Err(e :: Nil) + case (Err(e1), Err(e2)) => Err(e1 :: e2 :: Nil) + + /** Validate both `r` and `other`; return a tuple of successes or a list of failures. + * Unlike with `*`, the right hand side `other` must be a `Result` returning a `Tuple`, + * and the left hand side is added to it. See `Result.empty` for a convenient + * right unit of chains of `*:`s. + */ + def *: [U <: Tuple](other: Result[U, List[E]]): Result[T *: U, List[E]] = (r, other) match + case (Ok(x), Ok(ys)) => Ok(x *: ys) + case (Ok(_), es: Err[?]) => es + case (Err(e), Ok(_)) => Err(e :: Nil) + case (Err(e), Err(es)) => Err(e :: es) + + /** Simlar to `Try`: Convert exceptions raised by `body` to `Err`s. + * In principle, `Try[T]` should be equivalent to `Result[T, Exception]`. + * Note that we do not want to catch and reify all Throwables. + * - severe JVM errors that make continuation impossible should not be reified. + * - control throwables like `boundary.Break` should not be caught. We want + * them to return from a `Result`. + * (Generally, the focus on `Throwable` in Scala libraries is a mistake. + * Use `Exception` instead, as it was meant to in Java.) + */ + inline def apply[T](body: => T): Result[T, Exception] = + try Ok(body) + catch case ex: Exception => Err(ex) + + /** Right unit for chains of `*:`s. Returns an `Ok` with an `EmotyTuple` value. */ + def empty: Result[EmptyTuple, Nothing] = Ok(EmptyTuple) +end Result + +/** A prompt for `_.?`. It establishes a boundary to which `_.?` returns */ +object respond: + transparent inline def apply[T, E](inline body: Label[Err[E]] ?=> T): Result[T, E] = + boundary(Ok(body)) + diff --git a/tests/run/errorhandling/Test.scala b/tests/run/errorhandling/Test.scala new file mode 100644 index 000000000000..ef74da885ddb --- /dev/null +++ b/tests/run/errorhandling/Test.scala @@ -0,0 +1,78 @@ +import dotty.util.* + +/** boundary/break as a replacement for non-local returns */ +def indexOf[T](xs: List[T], elem: T): Int = + boundary: + for (x, i) <- xs.zipWithIndex do + if x == elem then break(i) + -1 + +def breakTest() = + println("breakTest") + assert(indexOf(List(1, 2, 3), 2) == 1) + assert(indexOf(List(1, 2, 3), 0) == -1) + +/** traverse becomes trivial to write */ +def traverse[T](xs: List[Option[T]]): Option[List[T]] = + optional(xs.map(_.?)) + +def optTest() = + println("optTest") + assert(traverse(List(Some(1), Some(2), Some(3))) == Some(List(1, 2, 3))) + assert(traverse(List(Some(1), None, Some(3))) == None) + +/** A check function returning a Result[Unit, _] */ +inline def check[E](p: Boolean, err: E): Result[Unit, E] = + if p then Ok(()) else Err(err) + +/** Another variant of a check function that returns directly to the given + * label in case of error. + */ +inline def check_![E](p: Boolean, err: E)(using l: boundary.Label[Err[E]]): Unit = + if p then () else l.break(Err(err)) + +/** Use `Result` to convert exceptions to `Err` values */ +def parseDouble(s: String): Result[Double, Exception] = + Result(s.toDouble) + +def parseDoubles(ss: List[String]): Result[List[Double], Exception] = + respond: + ss.map(parseDouble(_).?) + +/** Demonstrate combination of `check` and `.?`. */ +def trySqrt(x: Double) = // inferred: Result[Double, String] + respond: + check(x >= 0, s"cannot take sqrt of negative $x").? + math.sqrt(x) + +/** Instead of `check(...).?` one can also use `check_!(...)`. + * Note use of `mapErr` to convert Exception errors to String errors. + */ +def sumRoots(xs: List[String]) = // inferred: Result[Double, String] + respond: + check_!(xs.nonEmpty, "list is empty") // direct jump + val ys = parseDoubles(xs).mapErr(_.toString).? // direct jump + ys.reduce((x, y) => x + trySqrt(y).?) // need exception to propagate `Err` + +def resultTest() = + println("resultTest") + def assertFail(value: Any, s: String) = value match + case Err(msg: String) => assert(msg.contains(s)) + assert(sumRoots(List("1", "4", "9")) == Ok(6)) + assertFail(sumRoots(List("1", "-2", "4")), "cannot take sqrt of negative") + assertFail(sumRoots(List()), "list is empty") + assertFail(sumRoots(List("1", "3ab")), "NumberFormatException") + val xs = sumRoots(List("1", "-2", "4")) *: sumRoots(List()) *: sumRoots(List("1", "3ab")) *: Result.empty + xs match + case Err(msgs) => assert(msgs.length == 3) + case _ => assert(false) + val ys = sumRoots(List("1", "2", "4")) *: sumRoots(List("1")) *: sumRoots(List("2")) *: Result.empty + ys match + case Ok((a, b, c)) => // ok + case _ => assert(false) + +@main def Test = + breakTest() + optTest() + resultTest() + parseCsvIgnoreErrors() \ No newline at end of file diff --git a/tests/run/errorhandling/break.scala b/tests/run/errorhandling/break.scala new file mode 100644 index 000000000000..604d5ba888f7 --- /dev/null +++ b/tests/run/errorhandling/break.scala @@ -0,0 +1,22 @@ +package dotty.util +import scala.util.control.ControlThrowable + +object boundary: + + class Break[T](val label: Label[T], val value: T) extends ControlThrowable + + class Label[T] extends ControlThrowable: + transparent inline def break(value: T): Nothing = throw Break(this, value) + + transparent inline def apply[T <: R, R](inline body: Label[T] ?=> R): R = + val local = Label[T]() + try body(using local) + catch case ex: Break[T] @unchecked => + if ex.label eq local then ex.value + else throw ex + +end boundary + +object break: + transparent inline def apply[T](value: T)(using l: boundary.Label[T]): Nothing = + l.break(value) diff --git a/tests/run/errorhandling/kostas.scala b/tests/run/errorhandling/kostas.scala new file mode 100644 index 000000000000..ec32750f4bc4 --- /dev/null +++ b/tests/run/errorhandling/kostas.scala @@ -0,0 +1,35 @@ +package optionMockup: + import dotty.util.boundary + object optional: + transparent inline def apply[T](inline body: boundary.Label[None.type] ?=> T): Option[T] = + boundary(Some(body)) + + extension [T](r: Option[T]) + transparent inline def ? (using label: boundary.Label[None.type]): T = r match + case Some(x) => x + case None => label.break(None) + +import optionMockup.* + +case class Person(name: String, age: Int) + +object PersonCsvParserIgnoreErrors: + def parse(csv: Seq[String]): Seq[Person] = + for + line <- csv + columns = line.split(",") + parsed <- parseColumns(columns) + yield + parsed + + private def parseColumns(columns: Seq[String]): Option[Person] = + columns match + case Seq(name, age) => parsePerson(name, age) + case _ => None + + private def parsePerson(name: String, age: String): Option[Person] = + optional: + Person(name, age.toIntOption.?) + +def parseCsvIgnoreErrors() = + println(PersonCsvParserIgnoreErrors.parse(Seq("Kostas,5", "George,invalid", "too,many,columns")).mkString("\n")) \ No newline at end of file diff --git a/tests/run/errorhandling/optional.scala b/tests/run/errorhandling/optional.scala new file mode 100644 index 000000000000..5dee9cb21996 --- /dev/null +++ b/tests/run/errorhandling/optional.scala @@ -0,0 +1,20 @@ +package dotty.util +import boundary.Label + +/** A mockup of scala.Option */ +abstract class Option[+T] +case class Some[+T](x: T) extends Option[T] +case object None extends Option[Nothing] + +object Option: + /** This extension should be added to the companion object of scala.Option */ + extension [T](r: Option[T]) + transparent inline def ? (using label: Label[None.type]): T = r match + case Some(x) => x + case None => label.break(None) + +/** A prompt for `Option`, which establishes a boundary which `_.?` on `Option` can return */ +object optional: + transparent inline def apply[T](inline body: Label[None.type] ?=> T): Option[T] = + boundary(Some(body)) +