Last active
October 15, 2024 07:21
-
-
Save johnhungerford/d6a50d31c7b9e05729b801d276977a6b to your computer and use it in GitHub Desktop.
Kotlin/TypeScript -like syntax for accessing nested optional types
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//> 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