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

rescala.operator.Signal.scala Maven / Gradle / Ivy

There is a newer version: 0.35.1
Show newest version
package rescala.operator

import rescala.compat.SignalCompatBundle
import rescala.operator.RExceptions.{EmptySignalControlThrowable, ObservedException}

import scala.concurrent.{ExecutionContext, Future}
import scala.util.Success
import scala.util.control.NonFatal

object SignalMacroImpl {
  object MapFuncImpl { def apply[T1, A](value: T1, mapper: T1 => A): A = mapper(value) }
}

trait SignalBundle extends SignalCompatBundle {
  selfType: Operators =>

  /** Time changing value derived from the dependencies.
    *
    * @tparam T Type stored by the signal
    * @groupname operator Signal operators
    * @groupprio operator 10
    * @groupname conversion Signal to Event conversions
    * @groupprio conversion 20
    * @groupname accessors Accessors and observers
    * @groupprio accessor 5
    */
  trait Signal[+T] extends Disconnectable with SignalCompat[T] {
    override type Value <: Pulse[T]
    override def read(v: Value): T                             = v.get
    override protected[rescala] def commit(base: Value): Value = base
    def resource: ReadAs[T]                                    = this

    /** Returns the current value of the signal
      * However, using now is in most cases not what you want.
      * It does not build dependencies, does not integrate into transactions.
      * Use only for examples and debug output.
      *
      * @group accessor
      */
    final def now(implicit scheduler: Scheduler): T = readValueOnce

    /** Returns the current value of the signal
      *
      * @group accessor
      */
    final def readValueOnce(implicit scheduler: Scheduler): T = {
      RExceptions.toExternalReadException(this, scheduler.singleReadValueOnce(this))
    }

    /** add an observer
      *
      * @group accessor
      */
    final def observe(onValue: T => Unit, onError: Throwable => Unit = null, fireImmediately: Boolean = true)(implicit
        ticket: CreationTicket
    ): Disconnectable =
      Observe.strong(this, fireImmediately) { reevalVal =>
        new ObserveInteract {
          override def checkExceptionAndRemoval(): Boolean = {
            reevalVal match {
              case Pulse.empty => ()
              case Pulse.Exceptional(f) if onError == null =>
                throw ObservedException(Signal.this.resource, "observed", f)
              case _ => ()
            }
            false
          }

          override def execute(): Unit =
            (reevalVal: Pulse[T]) match {
              case Pulse.empty          => ()
              case Pulse.Value(v)       => onValue(v)
              case Pulse.Exceptional(f) => onError(f)
              case Pulse.NoChange       => ()
            }
        }
      }

    /** Uses a partial function `onFailure` to recover an error carried by the event into a value. */
    @cutOutOfUserComputation
    final def recover[R >: T](onFailure: PartialFunction[Throwable, R])(implicit ticket: CreationTicket): Signal[R] =
      Signals.static(this.resource) { st =>
        try st.dependStatic(this.resource)
        catch {
          case NonFatal(e) => onFailure.applyOrElse[Throwable, R](e, throw _)
        }
      }

    // ================== Derivations ==================

    // final def recover[R >: A](onFailure: Throwable => R)(implicit ticket: TurnSource): Signal[R, S = recover(PartialFunction(onFailure))

    @cutOutOfUserComputation
    final def abortOnError(message: String)(implicit ticket: CreationTicket): Signal[T] =
      recover { case t => throw ObservedException(this.resource, s"forced abort ($message)", t) }

    @cutOutOfUserComputation
    final def withDefault[R >: T](value: R)(implicit ticket: CreationTicket): Signal[R] =
      Signals.static(this.resource) { st =>
        try st.dependStatic(this.resource)
        catch {
          case EmptySignalControlThrowable => value
        }
      }

    /** Flattens the inner value.
      * @group operator
      */
    @cutOutOfUserComputation
    final def flatten[R](implicit flatten: Flatten[Signal[T], R]): R = flatten.apply(this)

    /** Create an event that fires every time the signal changes. It fires the tuple (oldVal, newVal) for the signal.
      * Be aware that no change will be triggered when the signal changes to or from empty
      *
      * @group conversion
      */
    @cutOutOfUserComputation
    final def change(implicit ticket: CreationTicket): Event[Diff[T]] = Events.change(this)(ticket)

    /** Create an event that fires every time the signal changes. The value associated
      * to the event is the new value of the signal
      *
      * @group conversion
      */
    @cutOutOfUserComputation
    final def changed(implicit ticket: CreationTicket): Event[T] =
      Events.staticNamed(s"(changed $this)", this.resource) { st =>
        st.collectStatic(this) match {
          case Pulse.empty => Pulse.NoChange
          case other       => other
        }
      }

    /** Convenience function filtering to events which change this reactive to value
      *
      * @group conversion
      */
    @cutOutOfUserComputation
    final def changedTo[V >: T](value: V)(implicit ticket: CreationTicket): Event[Unit] =
      Events.staticNamed(s"(filter $this)", this) { st =>
        st.collectStatic(this).filter(_ == value)
      }.dropParam

  }

  /** Functions to construct signals, you probably want to use signal expressions in [[rescala.interface.RescalaInterface.Signal]] for a nicer API. */
  object Signals {

    private def ignore2[Tick, Current, Res](f: Tick => Res): (Tick, Current) => Res = (ticket, _) => f(ticket)

    @cutOutOfUserComputation
    def ofUDF[T](udf: UserDefinedFunction[T, ReSource, DynamicTicket])(implicit ct: CreationTicket): Signal[T] = {
      ct.create[Pulse[T], SignalImpl[T]](udf.staticDependencies, Pulse.empty, needsReevaluation = true) {
        state =>
          new SignalImpl[T](
            state,
            ignore2(udf.expression),
            ct.rename,
            if (udf.isStatic) None else Some(udf.staticDependencies)
          )
      }
    }

    /** creates a new static signal depending on the dependencies, reevaluating the function */
    @cutOutOfUserComputation
    def static[T](dependencies: ReSource*)(expr: StaticTicket => T)(implicit
        ct: CreationTicket
    ): Signal[T] = {
      ct.create[Pulse[T], SignalImpl[T]](dependencies.toSet, Pulse.empty, needsReevaluation = true) {
        state => new SignalImpl[T](state, ignore2(expr), ct.rename, None)
      }
    }

    /** creates a new static signal depending on the dependencies, reevaluating the function */
    @cutOutOfUserComputation
    def staticNoVarargs[T](dependencies: Seq[ReSource])(expr: StaticTicket => T)(implicit
        ct: CreationTicket
    ): Signal[T] = static(dependencies: _*)(expr)

    /** creates a signal that has dynamic dependencies (which are detected at runtime with Signal.apply(turn)) */
    @cutOutOfUserComputation
    def dynamic[T](dependencies: ReSource*)(expr: DynamicTicket => T)(implicit
        ct: CreationTicket
    ): Signal[T] = {
      val staticDeps = dependencies.toSet
      ct.create[Pulse[T], SignalImpl[T]](staticDeps, Pulse.empty, needsReevaluation = true) {
        state => new SignalImpl[T](state, ignore2(expr), ct.rename, Some(staticDeps))
      }
    }

    def fold[T](dependencies: Set[ReSource], init: T)(expr: StaticTicket => (() => T) => T)(implicit
                                                                                            ticket: CreationTicket
    ): Signal[T] = {
      ticket.create(
        dependencies,
        Pulse.tryCatch[T](Pulse.Value(init)),
        needsReevaluation = false
        ) {
        state => new SignalImpl[T](state, (st, v) => expr(st)(v), ticket.rename, None)
      }
    }


    /** creates a new static signal depending on the dependencies, reevaluating the function */
    @cutOutOfUserComputation
    def stateful[T](dependencies: ReSource*)(expr: StaticTicket => T)(implicit
        ct: CreationTicket
    ): Signal[T] = {
      ct.create[Pulse[T], SignalImpl[T]](dependencies.toSet, Pulse.empty, needsReevaluation = true) {
        state => new SignalImpl[T](state, ignore2(expr), ct.rename, None)
      }
    }

    /** creates a signal that has dynamic dependencies (which are detected at runtime with Signal.apply(turn)) */
    @cutOutOfUserComputation
    def dynamicNoVarargs[T](dependencies: Seq[ReSource])(expr: DynamicTicket => T)(implicit
        ct: CreationTicket
    ): Signal[T] = dynamic(dependencies: _*)(expr)

    /** converts a future to a signal */
    @cutOutOfUserComputation
    def fromFuture[A](fut: Future[A])(implicit fac: Scheduler, ec: ExecutionContext): Signal[A] = {
      fut.value match {
        case Some(Success(value)) => Var(value)(fac)
        case _ =>
          val v: Var[A] = Var.empty[A](fac)
          fut.onComplete { res =>
            fac.forceNewTransaction(v)(t => v.admitPulse(Pulse.tryCatch(Pulse.Value(res.get)))(t))
          }
          v
      }
    }

    @cutOutOfUserComputation
    def lift[A, R](los: Seq[Signal[A]])(fun: Seq[A] => R)(implicit maybe: CreationTicket): Signal[R] = {
      static(los.map(_.resource): _*) { t => fun(los.map(s => t.dependStatic(s.resource))) }
    }

    @cutOutOfUserComputation
    def lift[A1, B](n1: Signal[A1])(fun: A1 => B)(implicit maybe: CreationTicket): Signal[B] = {
      static(n1.resource)(t => fun(t.dependStatic(n1.resource)))
    }

    @cutOutOfUserComputation
    def lift[A1, A2, B](n1: Signal[A1], n2: Signal[A2])(fun: (A1, A2) => B)(implicit
        maybe: CreationTicket
    ): Signal[B] = {
      static(n1.resource, n2.resource)(t => fun(t.dependStatic(n1.resource), t.dependStatic(n2.resource)))
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy