Skip to content

Instantly share code, notes, and snippets.

@johnhungerford
Last active October 15, 2024 07:21
Show Gist options
  • Save johnhungerford/d6a50d31c7b9e05729b801d276977a6b to your computer and use it in GitHub Desktop.
Save johnhungerford/d6a50d31c7b9e05729b801d276977a6b to your computer and use it in GitHub Desktop.
Kotlin/TypeScript -like syntax for accessing nested optional types
//> 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment