All Downloads are FREE. Search and download functionalities are using the official Maven repository.

libretto.scaletto.Scaletto.scala Maven / Gradle / Ivy

The newest version!
package libretto.scaletto

import libretto.crash.CrashDSL
import libretto.puro.{Puro, PuroLib}
import libretto.timer.TimerDSL
import libretto.lambda.util.SourcePos
import libretto.util.Async
import scala.concurrent.duration.FiniteDuration
import scala.reflect.TypeTest

/** Supports manipulating Scala values via pure Scala functions.
  * Also extends [[TimerDSL]] and [[CrashDSL]],
  * since these are expected to be possible on a target platform that already supports Scala functions.
  */
trait Scaletto extends Puro with TimerDSL with CrashDSL {
  /** Scala value of type `A`.
    *
    * Somewhat analogous to [[scala.concurrent.Future]].
    */
  type Val[A]

  /** Demand for a Scala value of type `A`.
    *
    * Somewhat analogous to [[scala.concurrent.Promise]]
    */
  type Neg[A] = -[Val[A]]

  /** Resource that is a Scala object of type [[A]].
    * Unlike [[Val]], a resource can be mutable, cannot in general be [[neglect]]ed or [[dup]]licated, and is
    * automatically cleaned-up in case of crash.
    *
    * It is recommended to define custom opaque type aliases of resources, such as
    *
    * ```
    * opaque type Input = Res[java.io.InputStream]
    * ```
    */
  type Res[A]

  override val UInt31: UInt31Scaletto

  trait UInt31Scaletto extends UInt31s {
    def fromInt: Val[Int] -⚬ UInt31
    def toInt: UInt31 -⚬ Val[Int]
  }

  type ScalaFun[A, B]
  val ScalaFun: ScalaFuns

  trait ScalaFuns {
    def apply[A, B](f: A => B): ScalaFun[A, B]
    def blocking[A, B](f: A => B): ScalaFun[A, B]
    def async[A, B](f: A => Async[B]): ScalaFun[A, B]

    def adapt[A, B, Z, C](f: ScalaFun[A, B])(pre: Z => A, post: B => C): ScalaFun[Z, C]
    def adaptWith[A, B, Z, P, C](f: ScalaFun[A, B])(pre: Z => (P, A), post: (P, B) => C): ScalaFun[Z, C]
    def eval[A, B]: ScalaFun[(ScalaFun[A, B], A), B]
  }

  extension [A, B](f: ScalaFun[A, B]) {
    def adapt[Z, C](pre: Z => A, post: B => C): ScalaFun[Z, C] =
      ScalaFun.adapt(f)(pre, post)

    def adaptPre[Z](pre: Z => A): ScalaFun[Z, B] =
      ScalaFun.adapt(f)(pre, identity)

    def adaptPost[C](post: B => C): ScalaFun[A, C] =
      ScalaFun.adapt(f)(identity, post)

    def adaptWith[Z, P, C](pre: Z => (P, A), post: (P, B) => C): ScalaFun[Z, C] =
      ScalaFun.adaptWith(f)(pre, post)
  }

  private val lib = PuroLib(this)
  import lib.*

  /** Creates an entangled pair of demand ([[Neg]]) and supply ([[Val]]) such that when the demand is fulfilled
    * with a value, that value will be produced by the supply.
    */
  def promise[A]: One -⚬ (Neg[A] |*| Val[A]) =
    forevert[Val[A]]

  /** Uses the value (eventually) produced by [[Val]] to satisfy the demand of [[Neg]]. */
  def fulfill[A]: (Val[A] |*| Neg[A]) -⚬ One =
    backvert[Val[A]]

  def liftEither[A, B]: Val[Either[A, B]] -⚬ (Val[A] |+| Val[B])
  def unliftEither[A, B]: (Val[A] |+| Val[B]) -⚬ Val[Either[A, B]] =
    either(mapVal(Left(_)), mapVal(Right(_)))

  def liftPair[A, B]: Val[(A, B)] -⚬ (Val[A] |*| Val[B])
  def unliftPair[A, B]: (Val[A] |*| Val[B]) -⚬ Val[(A, B)]

  def liftNegPair[A, B]: Neg[(A, B)] -⚬ (Neg[A] |*| Neg[B]) =
    introFst(parFromOne(promise[A], promise[B]) > IXI > snd(unliftPair)) > assocLR > elimSnd(fulfill)

  def unliftNegPair[A, B]: (Neg[A] |*| Neg[B]) -⚬ Neg[(A, B)] =
    introFst(promise[(A, B)] > snd(liftPair)) > assocLR > elimSnd(IXI > parToOne(fulfill, fulfill))

  def mapVal[A, B](f: ScalaFun[A, B]): Val[A] -⚬ Val[B]

  /** Lifts an ordinary Scala function to a linear function on [[Val]]s. */
  def mapVal[A, B](f: A => B): Val[A] -⚬ Val[B] =
    mapVal(ScalaFun(f))

  /** Lifts an ordinary Scala function to a linear function on demands, in opposite direction. */
  def contramapNeg[A, B](f: A => B): Neg[B] -⚬ Neg[A] =
    contrapositive(mapVal(f))

  def constVal[A](a: A): Done -⚬ Val[A]
  def constNeg[A](a: A): Neg[A] -⚬ Need

  transparent inline def constVal[A]: Done -⚬ Val[A] =
    constVal(scala.compiletime.constValue[A])

  transparent inline def constNeg[A]: Neg[A] -⚬ Need =
    constNeg(scala.compiletime.constValue[A])

  def neglect[A]: Val[A] -⚬ Done
  def inflate[A]: Need -⚬ Neg[A] =
    introFst(promise[A] > snd(neglect)) > assocLR > elimSnd(rInvertSignal)

  def notifyVal[A]: Val[A] -⚬ (Ping |*| Val[A])
  def notifyNeg[A]: (Pong |*| Neg[A]) -⚬ Neg[A]

  def dup[A]: Val[A] -⚬ (Val[A] |*| Val[A]) =
    mapVal[A, (A, A)](a => (a, a)) > liftPair

  def dupNeg[A]: (Neg[A] |*| Neg[A]) -⚬ Neg[A] =
    unliftNegPair[A, A] > contramapNeg(a => (a, a))

  def switchVal[A, R](
    a: $[Val[A]],
    cases: ValSwitch.Cases[A, A, R],
  )(pos: SourcePos)(using
    LambdaContext,
  ): $[R]

  def switch[A](using pos: SourcePos)(a: $[Val[A]])(using LambdaContext): ValSwitchInit[A] =
    ValSwitchInit(a, pos)

  def delay: Val[FiniteDuration] -⚬ Done

  def delayNeed: Need -⚬ Neg[FiniteDuration] =
    id                                          [                                                    Need  ]
      ./>(introFst(promise[FiniteDuration])) .to[ (Neg[FiniteDuration] |*|  Val[FiniteDuration]) |*| Need  ]
      ./>(assocLR)                           .to[  Neg[FiniteDuration] |*| (Val[FiniteDuration]  |*| Need) ]
      ./>.snd.fst(delay)                     .to[  Neg[FiniteDuration] |*| (  Done               |*| Need) ]
      ./>(elimSnd(rInvertSignal))            .to[  Neg[FiniteDuration]                                     ]

  override def delay(d: FiniteDuration): Done -⚬ Done =
    constVal(d) > delay

  def acquire[A, R, B](
    acquire: ScalaFun[A, (R, B)],
    release: Option[ScalaFun[R, Unit]],
  ): Val[A] -⚬ (Res[R] |*| Val[B])

  /** Acquires a resource of type [[R]].
    *
    * @param acquire
    * @param release called to release the resource in case of a crash. `None` means no cleanup is needed
    * @tparam A parameters of the `acquire` function
    * @tparam R type of the resource
    * @tparam B additional data produced by acquiring the resource
    */
  def acquire[A, R, B](
    acquire: A => (R, B),
    release: Option[R => Unit],
  ): Val[A] -⚬ (Res[R] |*| Val[B]) =
    this.acquire(ScalaFun(acquire), release.map(ScalaFun(_)))

  def acquireAsync[A, R, B](
    acquire: A => Async[(R, B)],
    release: Option[R => Async[Unit]],
  ): Val[A] -⚬ (Res[R] |*| Val[B]) =
    this.acquire(ScalaFun.async(acquire), release.map(ScalaFun.async(_)))

  /** Acquires a resource of type [[R]]. Might fail with an error of type [[E]].
    *
    * @param acquire
    * @param release called to release the resource in case of a crash. `None` means no cleanup is needed
    * @tparam A parameters of the `acquire` function
    * @tparam R type of the resource
    * @tparam B additional data produced by acquiring the resource
    * @tparam E type of the error
    */
  def tryAcquire[A, R, B, E](
    acquire: A => Either[E, (R, B)],
    release: Option[R => Unit],
  ): Val[A] -⚬ (Val[E] |+| (Res[R] |*| Val[B])) =
    tryAcquire(
      ScalaFun(acquire),
      release.map(ScalaFun(_)),
    )

  def tryAcquire[A, R, B, E](
    acquire: ScalaFun[A, Either[E, (R, B)]],
    release: Option[ScalaFun[R, Unit]],
  ): Val[A] -⚬ (Val[E] |+| (Res[R] |*| Val[B]))

  /** Releases a resource using the `release` function registered during resource acquisition. */
  def release[R]: Res[R] -⚬ Done

  def releaseWith[R, A, B](f: ScalaFun[(R, A), B]): (Res[R] |*| Val[A]) -⚬ Val[B]

  /** Releases a resource using the given function. The `release` function previously registered during resource
    * acquisition is not used.
    *
    * @param f the release function
    * @tparam R type of the resource
    * @tparam A additional parameter of the release function
    * @tparam B additional data produced by the release function
    */
  def release[R, A, B](f: (R, A) => B): (Res[R] |*| Val[A]) -⚬ Val[B] =
    releaseWith(ScalaFun(f.tupled))

  def releaseAsync[R, A, B](f: (R, A) => Async[B]): (Res[R] |*| Val[A]) -⚬ Val[B] =
    releaseWith(ScalaFun.async(f.tupled))

  def effect[R, A, B](f: ScalaFun[(R, A), B]): (Res[R] |*| Val[A]) -⚬ (Res[R] |*| Val[B])

  /** Performs a (potentially) effectful operation on a resource, producing some output.
    *
    * @param f the effectful operation
    * @tparam R type of the resource
    * @tparam A additional parameter of the operation
    * @tparam B additional output of the operation
    */
  def effect[R, A, B](f: (R, A) => B): (Res[R] |*| Val[A]) -⚬ (Res[R] |*| Val[B]) =
    effect(ScalaFun(f.tupled))

  def effectAsync[R, A, B](f: (R, A) => Async[B]): (Res[R] |*| Val[A]) -⚬ (Res[R] |*| Val[B]) =
    effect(ScalaFun.async(f.tupled))

  def effectWr[R, A](f: ScalaFun[(R, A), Unit]): (Res[R] |*| Val[A]) -⚬ Res[R]

  /** Variant of [[effect]] that does not produce output in addition to performing the effect.
    * Can be viewed as ''wr''iting an [[A]] into the resource.
    */
  def effectWr[R, A](f: (R, A) => Unit): (Res[R] |*| Val[A]) -⚬ Res[R] =
    effectWr(ScalaFun(f.tupled))

  def effectWrAsync[R, A](f: (R, A) => Async[Unit]): (Res[R] |*| Val[A]) -⚬ Res[R] =
    effectWr(ScalaFun.async(f.tupled))

  def tryEffectAcquire[R, A, S, B, E](
    f: ScalaFun[(R, A), Either[E, (S, B)]],
    release: Option[ScalaFun[S, Unit]],
  ): (Res[R] |*| Val[A]) -⚬ (Res[R] |*| (Val[E] |+| (Res[S] |*| Val[B])))

  /** Transforms a resource into a resource of (possibly) different type.
    *
    * @param f the transformation function. It receives the input resource and additional input of type [[A]].
    *          It returns the new resource and additional output of type [[B]].
    * @param release called to release the new resource in case of a crash. `None` means no cleanup is needed
    * @tparam R type of the input resource
    * @tparam A additional parameter of the transformation
    * @tparam S type of the output resource
    * @tparam B additional output of the transformation
    */
  def transformResource[R, A, S, B](
    f: (R, A) => (S, B),
    release: Option[S => Unit],
  ): (Res[R] |*| Val[A]) -⚬ (Res[S] |*| Val[B]) =
    transformResourceAsync(
      (r, a) => Async.now(f(r, a)),
      release.map(_ andThen Async.now),
    )

  def transformResourceAsync[R, A, S, B](
    f: (R, A) => Async[(S, B)],
    release: Option[S => Async[Unit]],
  ): (Res[R] |*| Val[A]) -⚬ (Res[S] |*| Val[B]) =
    tryTransformResourceAsync[R, A, S, B, Nothing](
      (r, a) => f(r, a).map(Right(_)),
      release,
    )                                       .to[ Val[Nothing] |+| (Res[S] |*| Val[B]) ]
      > either(anyResourceFromNothing, id)  .to[                   Res[S] |*| Val[B]  ]

  def tryTransformResource[R, A, S, B, E](
    f: ScalaFun[(R, A), Either[E, (S, B)]],
    release: Option[ScalaFun[S, Unit]],
  ): (Res[R] |*| Val[A]) -⚬ (Val[E] |+| (Res[S] |*| Val[B]))

  /** Transforms a resource into a resource of (possibly) different type. Might fail with an error of type [[E]].
    *
    * @param f the transformation function. It receives the input resource and additional input of type [[A]].
    *          It returns either an error of type [[E]] or the new resource and additional output of type [[B]].
    *          In case the transformation results in an error, the original resource is ''not'' released automatically—
    *          the passing of the original resource `R` to the transformation function `f` indicates transfer of
    *          responsibility for the resource to the function `f`.
    * @param release called to release the new resource in case of a crash. `None` means no cleanup is needed
    * @tparam R type of the input resource
    * @tparam A additional parameter of the transformation
    * @tparam S type of the output resource
    * @tparam B additional output of the transformation
    * @tparam E type of the error
    */
  def tryTransformResource[R, A, S, B, E](
    f: (R, A) => Either[E, (S, B)],
    release: Option[S => Unit],
  ): (Res[R] |*| Val[A]) -⚬ (Val[E] |+| (Res[S] |*| Val[B])) =
    tryTransformResource(
      ScalaFun(f.tupled),
      release.map(ScalaFun(_)),
    )

  def tryTransformResourceAsync[R, A, S, B, E](
    f: (R, A) => Async[Either[E, (S, B)]],
    release: Option[S => Async[Unit]],
  ): (Res[R] |*| Val[A]) -⚬ (Val[E] |+| (Res[S] |*| Val[B])) =
    tryTransformResource(
      ScalaFun.async(f.tupled),
      release.map(ScalaFun.async(_)),
    )

  def splitResource[R, A, S, T, B](
    f: ScalaFun[(R, A), (S, T, B)],
    release1: Option[ScalaFun[S, Unit]],
    release2: Option[ScalaFun[T, Unit]],
  ): (Res[R] |*| Val[A]) -⚬ ((Res[S] |*| Res[T]) |*| Val[B]) =
    trySplitResource(
      ScalaFun.adapt(f)(identity[(R, A)], Right[Nothing, (S, T, B)]),
      release1,
      release2,
    ) > either(anyTwoResourcesFromNothing, id)

  def splitResource[R, A, S, T, B](
    f: (R, A) => (S, T, B),
    release1: Option[S => Unit],
    release2: Option[T => Unit],
  ): (Res[R] |*| Val[A]) -⚬ ((Res[S] |*| Res[T]) |*| Val[B]) =
    splitResource(
      ScalaFun(f.tupled),
      release1.map(ScalaFun(_)),
      release2.map(ScalaFun(_)),
    )

  def splitResourceAsync[R, A, S, T, B](
    f: (R, A) => Async[(S, T, B)],
    release1: Option[S => Async[Unit]],
    release2: Option[T => Async[Unit]],
  ): (Res[R] |*| Val[A]) -⚬ ((Res[S] |*| Res[T]) |*| Val[B]) =
      splitResource(
        ScalaFun.async(f.tupled),
        release1.map(ScalaFun.async(_)),
        release2.map(ScalaFun.async(_)),
      )

  def trySplitResource[R, A, S, T, B, E](
    f: ScalaFun[(R, A), Either[E, (S, T, B)]],
    release1: Option[ScalaFun[S, Unit]],
    release2: Option[ScalaFun[T, Unit]],
  ): (Res[R] |*| Val[A]) -⚬ (Val[E] |+| ((Res[S] |*| Res[T]) |*| Val[B]))

  def trySplitResource[R, A, S, T, B, E](
    f: (R, A) => Either[E, (S, T, B)],
    release1: Option[S => Unit],
    release2: Option[T => Unit],
  ): (Res[R] |*| Val[A]) -⚬ (Val[E] |+| ((Res[S] |*| Res[T]) |*| Val[B])) =
    trySplitResource(
      ScalaFun(f.tupled),
      release1.map(ScalaFun(_)),
      release2.map(ScalaFun(_)),
    )

  def trySplitResourceAsync[R, A, S, T, B, E](
    f: (R, A) => Async[Either[E, (S, T, B)]],
    release1: Option[S => Async[Unit]],
    release2: Option[T => Async[Unit]],
  ): (Res[R] |*| Val[A]) -⚬ (Val[E] |+| ((Res[S] |*| Res[T]) |*| Val[B])) =
    trySplitResource(
      ScalaFun.async(f.tupled),
      release1.map(ScalaFun.async(_)),
      release2.map(ScalaFun.async(_)),
    )


  private def anyResourceFromNothing[R, B]: Val[Nothing] -⚬ (Res[R] |*| Val[B])=
    acquire(x => x, release = None)

  private def anyTwoResourcesFromNothing[S, T, B]: Val[Nothing] -⚬ ((Res[S] |*| Res[T]) |*| Val[B]) =
    dup[Nothing]                                                                          .to[       Val[Nothing]        |*|          Val[Nothing]           ]
      .>( par(anyResourceFromNothing[S, Nothing], anyResourceFromNothing[T, Nothing]) )   .to[ (Res[S] |*| Val[Nothing]) |*| (   Res[T]    |*| Val[Nothing]) ]
      .>( IXI )                                                                           .to[ (Res[S] |*|    Res[T]   ) |*| (Val[Nothing] |*| Val[Nothing]) ]
      .>( par(id, unliftPair > mapVal(_._1)) )                                            .to[ (Res[S] |*|    Res[T]   ) |*|             Val[B]              ]

  /** Executes a potentially blocking operation.
   *  The runtime will ensure that the blocking operation does not impede
   *  any of the concurrently happening non-blocking computations.
   */
  def blocking[A, B](f: A => B): Val[A] -⚬ Val[B] =
    mapVal(ScalaFun.blocking(f))

  /** Prints the given message to the console, without creating an obligation to await. */
  def debugPrint(msg: String): Ping -⚬ One

  def constantVal[A](a: A)(using SourcePos, LambdaContext): $[Val[A]] =
    constant(done) |> constVal(a)

  def tuple[A, B](a: $[Val[A]], b: $[Val[B]])(using
    SourcePos,
    LambdaContext,
  ): $[Val[(A, B)]] =
    (a |*| b) |> unliftPair

  def tuple[A, B, C](a: $[Val[A]], b: $[Val[B]], c: $[Val[C]])(using
    SourcePos,
    LambdaContext,
  ): $[Val[(A, B, C)]] =
    tuple(tuple(a, b), c) |> mapVal { case ((a, b), c) => (a, b, c) }

  def tuple[A, B, C, D](a: $[Val[A]], b: $[Val[B]], c: $[Val[C]], d: $[Val[D]])(using
    SourcePos,
    LambdaContext,
  ): $[Val[(A, B, C, D)]] =
    tuple(tuple(a, b), tuple(c, d)) |> mapVal { case ((a, b), (c, d)) => (a, b, c, d) }

  def tuple[A, B, C, D, E](a: $[Val[A]], b: $[Val[B]], c: $[Val[C]], d: $[Val[D]], e: $[Val[E]])(using
    SourcePos,
    LambdaContext,
  ): $[Val[(A, B, C, D, E)]] =
    tuple(tuple(a, b, c), tuple(d, e)) |> mapVal { case ((a, b, c), (d, e)) => (a, b, c, d, e) }

  def tuple[A, B, C, D, E, F](a: $[Val[A]], b: $[Val[B]], c: $[Val[C]], d: $[Val[D]], e: $[Val[E]], f: $[Val[F]])(using
    SourcePos,
    LambdaContext,
  ): $[Val[(A, B, C, D, E, F)]] =
    tuple(tuple(a, b, c), tuple(d, e, f)) |> mapVal { case ((a, b, c), (d, e, f)) => (a, b, c, d, e, f) }

  class ValSwitchInit[A](a: $[Val[A]], pos: SourcePos)(using LambdaContext) {
    def Case[A0 <: A](using tt: TypeTest[A, A0], casePos: SourcePos): ValSwitchInitCase[A, A0] =
      ValSwitchInitCase[A, A0](a, pos, tt, casePos)
  }

  class ValSwitchInitCase[A, A0 <: A](a: $[Val[A]], pos: SourcePos, tt: TypeTest[A, A0], casePos: SourcePos)(using LambdaContext) {
    def apply[R](f: LambdaContext ?=> $[Val[A0]] => $[R]): ValSwitch[A, A0, R] =
      ValSwitch(a, pos, ValSwitch.FirstCase(tt, f, casePos))
  }

  /**
   *
   * @tparam A type of the scrutinee (the value to match on)
   * @tparam A0 subtype of A covered so far
   * @tparam R result type that each case must produce
   */
  class ValSwitch[A, A0, R](a: $[Val[A]], pos: SourcePos, cases: ValSwitch.Cases[A, A0, R])(using LambdaContext) {
    def endswitch(using ev: A0 =:= A): $[R] =
      switchVal[A, R](a, ev.substituteCo[[a] =>> ValSwitch.Cases[A, a, R]](cases))(pos)

    def Case[A1 <: A](using tt: TypeTest[A, A1], pos: SourcePos)(
      f: LambdaContext ?=> $[Val[A1]] => $[R],
    ): ValSwitch[A, A0 | A1, R] =
      ValSwitch(a, pos, ValSwitch.NextCase(cases, tt, f, pos))
  }

  object ValSwitch {
    sealed trait Cases[A, A0, R]

    class FirstCase[A, A0, R](
      val typeTest: TypeTest[A, A0],
      val f: LambdaContext ?=> $[Val[A0]] => $[R],
      val pos: SourcePos,
    ) extends Cases[A, A0, R]

    class NextCase[A, A0, A1, R](
      val base: Cases[A, A0, R],
      val typeTest: TypeTest[A, A1],
      val f: LambdaContext ?=> $[Val[A1]] => $[R],
      val pos: SourcePos,
    ) extends Cases[A, A0 | A1, R]
  }

  /** Returns the size of the given program, in further unspecified units.
   * Useful only for approximate relative comparisons.
   */
  def sizeOf[A, B](f: A -⚬ B): Long
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy