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

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

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

import rescala.compat.EventCompatBundle
import rescala.operator.Pulse.{Exceptional, NoChange, Value}
import rescala.operator.RExceptions.ObservedException

import scala.collection.immutable.{LinearSeq, Queue}

object EventsMacroImpl {
  object MapFuncImpl {
    def apply[T1, A](value: Option[T1], mapper: T1 => A): Option[A] =
      value.map(mapper)
  }
  object FilterFuncImpl {
    def apply[T1, A](value: Option[T1], filter: T1 => Boolean): Option[T1] =
      value.filter(filter)
  }
  object CollectFuncImpl {
    def apply[T1, A](value: Option[T1], filter: PartialFunction[T1, A]): Option[A] =
      value.collect(filter)
  }
  object FoldFuncImpl {
    def apply[T1, A](state: () => A, value: Option[T1], step: (A, T1) => A): A =
      value match {
        case None    => state()
        case Some(v) => step(state(), v)
      }
  }

}

trait EventBundle extends EventCompatBundle {
  selfType: Operators =>

  /** Events only propagate a value when they are changing,
    * when the system is at rest, events have no values.
    *
    * Note: We hide implicit parameters of the API in the documentation.
    * They are used to ensure correct creation, and you normally do not have to worry about them,
    * except if you accidentally call the implicit parameter list, in which cas you may get cryptic errors.
    * This is a scala limitation.
    * We also hide the internal state parameter of passed and returned events.
    *
    * @tparam T Value type of the event occurrences.
    *
    * @groupname operator Event operators
    * @groupprio operator 10
    * @groupname conversion Event to Signal conversions
    * @groupprio conversion 20
    * @groupname accessors Accessors and observers
    * @groupprio accessor 5
    */
  trait Event[+T] extends EventCompat[T] with Disconnectable {

    implicit def internalAccess(v: Value): Pulse[T]
    def resource: ReadAs[Option[T]] = this

    /** Interprets the pulse of the event by converting to an option
      *
      * @group internal
      */
    override def read(v: Value): Option[T] = v.toOption

    /** Adds an observer.
      * @see observe
      * @group accessor
      */
    final def +=(handler: T => Unit)(implicit ticket: CreationTicket): Disconnectable = observe(handler)(ticket)

    /** Add an observer.
      *
      * @return the resulting [[rescala.operator.ObserveBundle.Observe]] can be used to remove the observer.
      * @group accessor
      */
    final def observe(onValue: T => Unit, onError: Throwable => Unit = null, fireImmediately: Boolean = false)(implicit
        ticket: CreationTicket
    ): Disconnectable =
      Observe.strong(this, fireImmediately) { reevalVal =>
        val internalVal = internalAccess(reevalVal)
        new ObserveInteract {
          override def checkExceptionAndRemoval(): Boolean = {
            reevalVal match {
              case Pulse.Exceptional(f) if onError == null =>
                throw new ObservedException(Event.this, "observed", f)
              case _ => ()
            }
            false
          }
          override def execute(): Unit =
            internalVal match {
              case Pulse.NoChange       => ()
              case Pulse.Value(v)       => onValue(v)
              case Pulse.Exceptional(f) => onError(f)
            }
        }
      }

    /** Uses a partial function `onFailure` to recover an error carried by the event into a value when returning Some(value),
      * or filters the error when returning None
      */
    final def recover[R >: T](onFailure: PartialFunction[Throwable, Option[R]])(implicit
        ticket: CreationTicket
    ): Event[R] =
      Events.staticNamed(s"(recover $this)", this) { st =>
        st.collectStatic(this) match {
          case Exceptional(t) =>
            onFailure.applyOrElse[Throwable, Option[R]](t, throw _).fold[Pulse[R]](Pulse.NoChange)(Pulse.Value(_))
          case other => other
        }
      }

    /** Events disjunction.
      * Propagates the values if any of the events fires.
      * Only propagates the left event if both fire.
      * @group operator
      */
    @cutOutOfUserComputation
    final def ||[U >: T](other: Event[U])(implicit ticket: CreationTicket): Event[U] = {
      Events.staticNamed(s"(or $this $other)", this, other) { st =>
        val tp = st.collectStatic(this)
        if (tp.isChange) tp else other.internalAccess(st.collectStatic(other))
      }
    }

    /** Propagates the event only when except does not fire.
      * @group operator
      */
    @cutOutOfUserComputation
    final def \[U](except: Event[U])(implicit ticket: CreationTicket): Event[T] = {
      Events.staticNamed(s"(except $this  $except)", this, except) { st =>
        (except.internalAccess(st.collectStatic(except)): Pulse[U]) match {
          case NoChange            => st.collectStatic(this)
          case Value(_)            => Pulse.NoChange
          case ex @ Exceptional(_) => ex
        }
      }
    }

    /** Merge the event with the other, if both fire simultaneously.
      * @group operator
      */
    @cutOutOfUserComputation
    final def and[U, R](other: Event[U])(merger: (T, U) => R)(implicit ticket: CreationTicket): Event[R] = {
      Events.staticNamed(s"(and $this $other)", this, other) { st =>
        for {
          left  <- internalAccess(st.collectStatic(this)): Pulse[T]
          right <- other.internalAccess(st.collectStatic(other)): Pulse[U]
        } yield { merger(left, right) }
      }
    }

    /** Merge the event with the other into a tuple, if both fire simultaneously.
      * @group operator
      * @see and
      */
    @cutOutOfUserComputation
    final def zip[U](other: Event[U])(implicit ticket: CreationTicket): Event[(T, U)] = and(other)(Tuple2.apply)

    /** Merge the event with the other into a tuple, even if only one of them fired.
      * @group operator
      */
    @cutOutOfUserComputation
    final def zipOuter[U](other: Event[U])(implicit ticket: CreationTicket): Event[(Option[T], Option[U])] = {
      Events.staticNamed(s"(zipOuter $this $other)", this, other) { st =>
        val left: Pulse[T]  = st.collectStatic(this)
        val right: Pulse[U] = other.internalAccess(st.collectStatic(other))
        if (right.isChange || left.isChange) Value(left.toOption -> right.toOption) else NoChange
      }
    }

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

    /** Drop the event parameter; equivalent to map((_: Any) => ())
      * @group operator
      */
    @cutOutOfUserComputation
    final def dropParam(implicit ticket: CreationTicket): Event[Unit] =
      Events.static(this)(_ => Some(()))

    /** reduces events with a given reduce function to create a Signal
      *
      * @group conversion
      */
    @cutOutOfUserComputation
    final def reduce[A](reducer: (=> A, => T) => A)(implicit ticket: CreationTicket): Signal[A] = {
      ticket.create(
        Set(this),
        Pulse.empty: Pulse[A],
        needsReevaluation = false
      ) { state =>
        new SignalImpl[A](
          initial = state,
          expr = { (st, currentValue) => reducer(currentValue(), st.collectStatic(this).get) },
          name = ticket.rename,
          isDynamicWithStaticDeps = None
        )
      }
    }

    /** Applies a function on the current value of the signal every time the event occurs,
      * starting with the init value before the first event occurrence
      * @group conversion
      */
    @cutOutOfUserComputation
    final def iterate[A](init: A)(f: A => A)(implicit ticket: CreationTicket): Signal[A] =
      Events.foldOne(this, init)((acc, _) => f(acc))

    /** Counts the occurrences of the event.
      * The argument of the event is discarded.
      * Always starts from 0 when the count is created (no matter how often the event has activated in the past).
      * @group conversion
      */
    @cutOutOfUserComputation
    final def count()(implicit ticket: CreationTicket): Signal[Int] =
      Events.foldOne(this, 0)((acc, _) => acc + 1)

    /** returns a signal holding the latest value of the event.
      * @param init initial value of the returned signal
      * @group conversion
      */
    @cutOutOfUserComputation
    final def latest[A >: T](init: A)(implicit ticket: CreationTicket): Signal[A] =
      Events.foldOne(this, init)((_, v) => v)

    /** returns a signal holding the latest value of the event.
      * @group conversion
      */
    @cutOutOfUserComputation
    final def latest[A >: T]()(implicit ticket: CreationTicket): Signal[A] =
      reduce[A]((_, v) => v)

    /** Holds the latest value of an event as an Option, None before the first event occured
      * @group conversion
      */
    @cutOutOfUserComputation
    final def latestOption[A >: T]()(implicit ticket: CreationTicket): Signal[Option[A]] =
      Events.foldOne(this, None: Option[A]) { (_, v) => Some(v) }

    /** Returns a signal which holds the last n events in a list. At the beginning the
      * list increases in size up to when n values are available
      * @group conversion
      */
    @cutOutOfUserComputation
    final def last[A >: T](n: Int)(implicit ticket: CreationTicket): Signal[LinearSeq[A]] = {
      if (n < 0) throw new IllegalArgumentException(s"length must be positive")
      else if (n == 0) Var(Nil)
      else
        Events.foldOne(this, Queue[A]()) { (queue: Queue[A], v: T) =>
          if (queue.lengthCompare(n) >= 0) queue.tail.enqueue(v) else queue.enqueue(v)
        }
    }

    /** collects events resulting in a variable holding a list of all values.
      * @group conversion
      */
    @cutOutOfUserComputation
    final def list[A >: T]()(implicit ticket: CreationTicket): Signal[List[A]] =
      Events.foldOne(this, List[A]())((acc, v) => v :: acc)

    /** Switch back and forth between two signals on occurrence of event e
      * @group conversion
      */
    @cutOutOfUserComputation
    final def toggle[A](a: Signal[A], b: Signal[A])(implicit ticket: CreationTicket): Signal[A] =
      ticket.scope.embedTransaction { ict =>
        val switched: Signal[Boolean] = iterate(false) { !_ }(ict)
        Signals.dynamic(switched, a, b) { s => if (s.depend(switched)) s.depend(b) else s.depend(a) }(ict)
      }

  }

  object Events {

    /** the basic method to create static events */
    @cutOutOfUserComputation
    def staticNamed[T](name: String, dependencies: ReSource*)(expr: StaticTicket => Pulse[T])(implicit
        ticket: CreationTicket
    ): Event[T] = {
      ticket.create[Pulse[T], EventImpl[T]](dependencies.toSet, Pulse.NoChange, needsReevaluation = false) {
        state => new EventImpl[T](state, expr, name, None)
      }
    }

    /** Creates static events */
    @cutOutOfUserComputation
    def static[T](dependencies: ReSource*)(expr: StaticTicket => Option[T])(implicit
        ticket: CreationTicket
    ): Event[T] =
      staticNamed(ticket.rename.str, dependencies: _*)(st => Pulse.fromOption(expr(st)))

    /** Creates static events */
    @cutOutOfUserComputation
    def staticNoVarargs[T](dependencies: Seq[ReSource])(expr: StaticTicket => Option[T])(implicit
        ticket: CreationTicket
    ): Event[T] = static(dependencies: _*)(expr)

    /** Creates dynamic events */
    @cutOutOfUserComputation
    def dynamic[T](dependencies: ReSource*)(expr: DynamicTicket => Option[T])(implicit
        ticket: CreationTicket
    ): Event[T] = {
      val staticDeps = dependencies.toSet
      ticket.create[Pulse[T], EventImpl[T]](staticDeps, Pulse.NoChange, needsReevaluation = true) { state =>
        new EventImpl[T](state, expr.andThen(Pulse.fromOption), ticket.rename, Some(staticDeps))
      }
    }

    /** Creates dynamic events */
    @cutOutOfUserComputation
    def dynamicNoVarargs[T](dependencies: Seq[ReSource])(expr: DynamicTicket => Option[T])(implicit
        ticket: CreationTicket
    ): Event[T] = dynamic(dependencies: _*)(expr)

    /** Creates change events */
    @cutOutOfUserComputation
    def change[T](signal: Signal[T])(implicit ticket: CreationTicket): Event[Diff[T]] =
      ticket.scope.embedTransaction { tx =>
        val internal = tx.initializer.create[(Pulse[T], Pulse[Diff[T]]), ChangeEventImpl[T]](
          Set[ReSource](signal),
          (Pulse.NoChange, Pulse.NoChange),
          needsReevaluation = true
        ) { state =>
          new ChangeEventImpl[T](state, signal, ticket.rename)
        }
        static(internal)(st => st.dependStatic(internal))(tx)
      }

    @cutOutOfUserComputation
    def foldOne[A, T](dependency: Event[A], init: T)(expr: (T, A) => T)(implicit
        ticket: CreationTicket
    ): Signal[T] = {
      fold(Set[ReSource](dependency), init) { st => acc =>
        val a: A = dependency.internalAccess(st.collectStatic(dependency)).get
        expr(acc(), a)
      }
    }

    /** Folds events with a given operation to create a Signal.
      *
      * @see [[rescala.operator.EventBundle.Event.fold]]
      */
    @cutOutOfUserComputation
    def fold[T](dependencies: Set[ReSource], init: T)(expr: DynamicTicket => (() => 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)
      }
    }

    /** Folds when any one of a list of events occurs, if multiple events occur, every fold is executed in order.
      *
      * Example for a counter that can be reset:
      *
      * {{{
      * val add: Event[Int]
      * val reset: Event[Unit]
      * Events.foldAll(0){ current => Seq(
      *   add act2 { v => current + v },
      *   reset act2 { _ => 0 }
      * )}
      * }}}
      */
    @cutOutOfUserComputation
    final def foldAll[A](init: A)(accthingy: (=> A) => Seq[FoldMatch[A]])(implicit
        ticket: CreationTicket
    ): Signal[A] = {
      var acc = () => init
      val ops = accthingy(acc())
      val staticInputs = ops.collect {
        case fm: StaticFoldMatch[_, _]        => fm.event
        case fm: StaticFoldMatchDynamic[_, _] => fm.event
      }.toSet

      def operator(dt: DynamicTicket, oldValue: () => A): A = {
        acc = oldValue

        def applyToAcc[T](f: T => A, value: Option[T]): Unit = {
          value.foreach { v =>
            val res = f(v)
            acc = () => res
          }
        }

        ops.foreach {
          case fm: StaticFoldMatch[_, A] =>
            applyToAcc(fm.f, dt.dependStatic(fm.event))
          case fm: StaticFoldMatchDynamic[_, A] =>
            applyToAcc(fm.f(dt), dt.dependStatic(fm.event))
          case fm: DynamicFoldMatch[_, A] =>
            fm.event().map(dt.depend).foreach { applyToAcc(fm.f, _) }
        }
        acc()
      }

      ticket.create(
        staticInputs.toSet[ReSource],
        Pulse.tryCatch[A](Pulse.Value(init)),
        needsReevaluation = true
      ) {
        state => new SignalImpl[A](state, operator, ticket.rename, Some(staticInputs.toSet[ReSource]))
      }
    }

    sealed trait FoldMatch[+A]
    class StaticFoldMatch[T, +A](val event: Event[T], val f: T => A)                         extends FoldMatch[A]
    class StaticFoldMatchDynamic[T, +A](val event: Event[T], val f: DynamicTicket => T => A) extends FoldMatch[A]
    class DynamicFoldMatch[T, +A](val event: () => Seq[Event[T]], val f: T => A)             extends FoldMatch[A]

    class OnEv[T](event: Event[T]) {

      /** Constructs a handling branch that handles the static `event`
        * @param fun handler for activations of `event`
        */
      final def act2[A](fun: T => A): FoldMatch[A] = new StaticFoldMatch(event, fun)

      /** Similar to act2, but provides access to a dynamic ticket in `fun` */
      final def dyn2[A](fun: DynamicTicket => T => A): FoldMatch[A] = new StaticFoldMatchDynamic(event, fun)
    }

    class OnEvs[T](events: => Seq[Event[T]]) {

      /** Constructs a handler for all `events`, note that `events` may dynamically compute the events from the current state */
      final def act2[A](fun: T => A): FoldMatch[A] = new DynamicFoldMatch(() => events, fun)
    }

    class CBResult[T, R](val event: Event[T], val data: R)
    final class FromCallbackT[T] private[Events] (val dummy: Boolean = true) {
      def apply[R](body: (T => Unit) => R)(implicit ct: CreationTicket, s: Scheduler): CBResult[T, R] = {
        val evt: Evt[T] = Evt[T]()(ct)
        val res         = body(evt.fire(_))
        new CBResult(evt, res)
      }
    }

    def fromCallback[T]: FromCallbackT[T] = new FromCallbackT[T]()
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy