//> using scala 3.3 import scala.util.{Failure, NotGiven, Success, Try, boundary} import boundary.{Label, break} import scala.annotation.targetName /** * Proof of concept implementation of a syntax similar to Kotlin and * typescript. Within the context provided by [[getEither]], you can call * `?` on any optional/failable type (currently supports [[Option]], * [[Either]], and [[Try]]) to "get" the value. `getEither` then returns * an [[Either]] of the final value or any error picked up along the way. * * Its performance is better than simply converting values to [[Either]] * and composing with `flatMap` because it uses boundary/break to shortcut * errors and it keeps successful values unboxed until right before it * returns. There are only two allocations: failures are converted to an * intermediate [[GetAble.Fail]] instance, and then at the end a success or * failure is converted to an [[Either]] */ object Main: def main(args: Array[String]): Unit = import getEither.? println(getEither(testObj1.?.there.?.g.?.four.?.fin)) println(getEither(testObj1.?.there.?.g.?.four)) println(getEither(testObj2.?.there.?.g.?.four.?.fin)) println(getEither(testObj2.?.there.?.g)) println(getEither(testObj3.?.there.?.g.?.four.?.fin)) println(getEither(testObj3.?.there)) println(getEither(testObj4.?.there.?.g.?.four.?.fin)) println(getEither(testObj4)) println(getEither(testObj5.?.there.?.g.?.four.?.fin)) // Failure types are composed in union val value: Either[String | Unit | Throwable, String] = getEither(testObj1.?.there.?.g.?.four.?.fin) private case class Obj1(hi: String, there: Option[Obj2]) private case class Obj2(o: Int, m: Boolean, g: Either[Throwable, Obj3]) private case class Obj3(four: Try[Obj4]) private case class Obj4(fin: String) private val testObj1 = Right(Obj1("hi", Some(Obj2(2, true, Right(Obj3(Success(Obj4("what?")))))))) private val testObj2 = Right(Obj1("hi", Some(Obj2(2, true, Right(Obj3(Failure(new Exception("what?")))))))) private val testObj3 = Right(Obj1("hi", Some(Obj2(2, true, Left(new Exception("what?")))))) private val testObj4 = Right(Obj1("hi", None)) private val testObj5 = Left[String, Obj1]("what?") object getEither: import GetAble.Fail inline def apply[L, A](inline body: Label[Fail[L] | A] ?=> Fail[L] | A)(using ng : NotGiven[Fail[Nothing] <:< A],fe: ToEither[A]): Either[L | fe.L, fe.R] = boundary(body) match case Fail(value) => Left(value.asInstanceOf[L]) case success => fe.toEither(success.asInstanceOf[A]) extension [V, AV, FV](t: V)(using getable: GetAble[V] { type F = FV; type A = AV }, b: boundary.Label[Fail[FV]]) /** Exits with `Fail` to next enclosing `getEither` boundary */ @targetName("questionMark") inline def ? : AV = getable.get(t) match case either: Fail[FV] => break(either) case value => value.asInstanceOf[AV] /** * Type class for extracting boxed values while preserving failures * * @tparam T extractable type */ sealed trait GetAble[T]: import GetAble.Fail type F type A def get(value: T): Fail[F] | A object GetAble: sealed case class Fail[+F](failure: F) transparent inline def apply[F](using GetAble[F]): GetAble[F] = summon[GetAble[F]] given opt[AV, O <: Option[AV]]: GetAble[O] with type F = Unit; type A = AV override def get(value: O): Fail[Unit] | AV = value.getOrElse(Fail(())) given tr[AV, T <: Try[AV]]: GetAble[T] with type F = Throwable; type A = AV override def get(value: T): Fail[Throwable] | AV = value.getOrElse(Fail(value.asInstanceOf[Failure[Nothing]].exception)) given either[FV, AV, E <: Either[FV, AV]]: GetAble[E] with type F = FV; type A = AV override def get(value: E): Fail[FV] | AV = value match case value: Left[FV, AV] => Fail(value.value) case value: Right[FV, AV] => value.value /** * Type class for converting different optional or failable types to * an `Either`. * * @tparam A value that is convertable to Either */ sealed trait ToEither[A]: type L type R def toEither(a: A): Either[L, R] trait LowPriorityToEither: given any[A]: ToEither[A] with type L = Nothing; type R = A override def toEither(a: A): Either[Nothing, A] = Right(a) object ToEither extends LowPriorityToEither: given opt[A, O <: Option[A]]: ToEither[O] with type L = Unit; type R = A def toEither(a: O): Either[Unit, A] = a.fold(Left(()))(Right.apply) given tr[A, T <: Try[A]]: ToEither[T] with type L = Throwable; type R = A override def toEither(a: T): Either[Throwable, A] = a.toEither given either[LV, RV, E <: Either[LV, RV]]: ToEither[E] with type L = LV; type R = RV override def toEither(a: E): Either[LV, RV] = a