mhtml.rx.scala Maven / Gradle / Ivy
package mhtml
/** Reactive value of type `A`. Automatically recalculate on dependency update. */
sealed trait Rx[+A] { self =>
import Rx._
/**
* Apply a function to each element of this `Rx`.
*
* ```
* val numbers: Rx[Int]
* val doubles: Rx[Int] = numbers.map(2.*)
* // numbers => 0 1 4 3 2 ...
* // doubles => 0 2 8 6 4 ...
* ```
*/
def map[B](f: A => B): Rx[B] = Map[A, B](this, f)
/**
* Dynamically switch between different `Rx`s according to the given
* function, applied on each element of this `Rx`. Each switch will cancel
* the subscriptions for the previous outgoing `Rx` and start a new
* subscription on the next `Rx`.
*
* Together with `Rx#map` and `Rx.apply`, flatMap forms a `Monad`. [Proof](https://github.com/OlivierBlanvillain/monadic-html/blob/master/monadic-rx-cats/src/main/scala/mhtml/cats.scala).
*/
def flatMap[B](f: A => Rx[B]): Rx[B] = FlatMap[A, B](this, f)
/**
* Create the Cartesian product of two `Rx`. The output tuple contains the
* latest values from each input `Rx`, which updates whenever the value from
* either input `Rx` update. This method is faster than combining `Rx`s using
* `for { a <- ra; b <- rb } yield (a, b)`.
*
* ```
* // r1 => 0 8 9 ...
* // r2 => 1 4 5 6 ...
* // zip => (0,1) (8,1) (8,4) (8,5) (8,6) (9,6) ...
* ```
*
* This method, together with `Rx.apply`, forms am `Applicative`.
* `|@|` syntax is available via the `monadic-rx-cats` package.
*/
def zip[B](other: Rx[B]): Rx[(A, B)] = Zip[A, B](this, other)
@deprecated("Renamed to zip", "0.3.3")
def product[B](other: Rx[B]): Rx[(A, B)] = zip(other)
/**
* Drop repeated value of this `Rx`.
*
* ```
* val numbers: Rx[Int]
* val noDups: Rx[Int] = numbers.dropRepeats
* // numbers => 0 0 3 3 5 5 5 4 ...
* // noDups => 0 3 5 4 ...
* ```
*
* Note: This could also be implemented in terms of keepIf, map, and foldp.
*/
def dropRepeats: Rx[A] = DropRep[A](this)
/**
* Merge two `Rx` into one. Updates coming from either of the incoming `Rx`
* trigger updates in the outgoing `Rx`. Upon creation, the outgoing `Rx`
* first receives the current value from this `Rx`, then from the other `Rx`.
*
* ```
* val r1: Rx[Int]
* val r2: Rx[Int]
* val merged: Rx[Int] = r1.merge(r2)
* // r1 => 0 8 3 ...
* // r2 => 1 4 3 ...
* // merged => 0 8 4 3 3 ...
* ```
*
* With this operation, `Rx` forms a `Semigroup`. [Proof](https://github.com/OlivierBlanvillain/monadic-html/blob/master/monadic-rx-cats/src/main/scala/mhtml/cats.scala).
* `|+|` syntax is available via the `monadic-rx-cats` package.
*/
def merge[B >: A](other: Rx[B]): Rx[B] = Merge[A, B](this, other)
/**
* Produces a `Rx` containing cumulative results of applying a binary
* operator to each element of this `Rx`, starting from a `seed` and the
* current value of the upstream `Rx`, and moving forward in time; no internal
* state is maintained.
*
* ```
* val numbers: Rx[Int]
* val folded: Rx[Int] = numbers.foldp(0)(_ + _)
* // numbers => 1 2 1 1 3 ...
* // folded => 1 3 4 5 8 ...
* ```
*/
def foldp[B](seed: B)(step: (B, A) => B): Rx[B] = Foldp[A, B](this, seed, step)
/**
* Returns a new `Rx` with updates fulfilling a predicate.
* If the first update is dropped, the default value is used instead.
*
* ```
* val numbers: Rx[Int]
* val even: Rx[Int] = numbers.keepIf(_ % 2 == 0)(-1)
* // numbers => 0 0 3 4 5 6 ...
* // even => 0 0 4 6 ...
* ```
*/
def keepIf[B >: A](f: B => Boolean)(b: B): Rx[B] =
this.collect[B] { case e if f(e) => e }(b)
/**
* Returns a new `Rx` without updates fulfilling a predicate.
* If the first update is dropped, the default value is used instead.
*
* ```
* val numbers: Rx[Int]
* val even: Rx[Int] = numbers.dropIf(_ % 2 == 0)(-1)
* // numbers => 0 0 3 4 5 6 ...
* // even => -1 3 5 ...
* ```
*/
def dropIf[B >: A](f: B => Boolean)(b: B): Rx[B] =
this.collect[B] { case e if !f(e) => e }(b)
/**
* Returns a new `Rx` with updates where `f` is defined and mapped by `f`.
* If the first update is dropped, the default value is used instead.
*/
def collect[B](f: PartialFunction[A, B])(b: B): Rx[B] = Collect[A, B](this, f, b)
/**
* Sample this `Rx` using another `Rx`: every time an event occurs on
* the second `Rx` the output updates with the latest value of this `Rx`.
*
* ```
* val r1: Rx[Char]
* val r2: Rx[Int]
* val sp: Rx[Int] = r2.sampleOn(r1)
* // r1 => u u u u ...
* // r2 => 1 2 3 4 ...
* // sp => 1 3 3 4 ...
* ```
*/
def sampleOn[B](other: Rx[B]): Rx[A] = SampleOn[A, B](this, other)
val impure: RxImpureOps[A] = RxImpureOps[A](this)
}
case class RxImpureOps[+A](self: Rx[A]) extends AnyVal {
/**
* Applies the side effecting function `f` to each element of this `Rx`.
* Returns an `Cancelable` which can be used to cancel the subscription.
* Omitting to canceling subscription can lead to memory leaks.
*
* If you use this in your code, you are probably doing it wrong.
*/
def run(effect: A => Unit): Cancelable = Rx.run(self)(effect)
/**
* Memoizes this `Rx` using an internal `Var`. This is only
* useful for optimizing an Rx graph, so that values generated
* by this `Rx` are computed only once and shared between all executions.
*/
@deprecated("This will eventually be made private and used under-the-hood, automatically.", "1.0.0")
def sharing: Rx[A] = Rx.Sharing[A](self)
}
object Rx {
/** Creates a constant `Rx`. */
def apply[A](v: A): Rx[A] = Var.create(v)(_ => Cancelable.empty)
final case class Map [A, B] (self: Rx[A], f: A => B) extends Rx[B]
final case class FlatMap [A, B] (self: Rx[A], f: A => Rx[B]) extends Rx[B]
final case class Zip [A, B] (self: Rx[A], other: Rx[B]) extends Rx[(A, B)]
final case class DropRep [A] (self: Rx[A]) extends Rx[A]
final case class Merge [A, B >: A](self: Rx[A], other: Rx[B]) extends Rx[B]
final case class Foldp [A, B] (self: Rx[A], seed: B, step: (B, A) => B) extends Rx[B]
final case class Collect [A, B] (self: Rx[A], f: PartialFunction[A, B], b: B) extends Rx[B]
final case class SampleOn[A, B] (self: Rx[A], other: Rx[B]) extends Rx[A]
final case class Imitate [A] (self: Var[A], other: Rx[A]) extends Rx[A]
final case class Sharing [A] (self: Rx[A]) extends Rx[A] {
// Should be Var[A], but gives GADT Skolem bug:
protected[Rx] val sharingMemo: Var[Any] = Var(None)
protected[Rx] def isSharing = !(sharingCancelable == Cancelable.empty)
protected[Rx] var sharingCancelable: Cancelable = Cancelable.empty
}
/**
* The `impure.run` interpreter. Traverses the `Rx` tree and registers
* callbacks to run the outer most effect according to documented semantics.
*/
def run[A](rx: Rx[A])(effect: A => Unit): Cancelable = rx match {
case Map(self, f) =>
run(self)(x => effect(f(x)))
case FlatMap(self, f) =>
var c1 = Cancelable.empty
val c2 = run(self) { b =>
val fa = f(b)
c1.cancel
c1 = run(fa)(effect)
}
Cancelable { () => c1.cancel; c2.cancel }
case Zip(self, other) =>
var go = false
var v1: Any = null
var v2: Any = null
val c1 = run(self) { a => v1 = a; if(go) effect((v1, v2)) }
val c2 = run(other) { b => v2 = b; if(go) effect((v1, v2)) }
go = true
effect((v1, v2))
Cancelable { () => c1.cancel; c2.cancel }
case DropRep(self) =>
var previous: Option[A] = None
run(self) { a =>
if (previous.forall(a.!=)) {
effect(a)
previous = Some(a)
}
}
case Merge(self, other) =>
val c1 = run(self)(effect)
val c2 = run(other)(effect)
Cancelable { () => c1.cancel; c2.cancel }
case Foldp(self, seed, step) =>
var b = seed
run(self) { a =>
// This is a GADT skolem. I'm sober. scalac is drunk; works in dotty
val next = step.asInstanceOf[(Any, Any) => A](b, a)
b = next
effect(next)
}
case Collect(self, f, fallback) =>
var first = true
run(self) { a =>
val out =
if (f.isDefinedAt(a))
effect(f(a))
else if(first)
effect(fallback)
else ()
first = false
out
}
case SampleOn(self, other) =>
var currentA: A = null.asInstanceOf[A]
val ca = run(self)(currentA = _)
val cb = run(other)(_ => effect(currentA))
Cancelable { () => ca.cancel; cb.cancel }
case Imitate(self, other) =>
if (!self.imitating) {
self.imitating = true
val cc = run(other) { a =>
// self: Var[A] and a: A, pattern matching is fucked up...
self.asInstanceOf[Var[Any]] := a
effect(a)
}
Cancelable { () => cc.cancel; self.imitating = false }
} else run(other)(effect)
case rx @ Sharing(self) =>
if (!rx.isSharing) {
rx.sharingCancelable = run(self)(rx.sharingMemo.:=)
}
val foreachCancelable = rx.sharingMemo.foreach(x => effect(x.asInstanceOf[A]))
Cancelable { () =>
foreachCancelable.cancel
if (rx.sharingMemo.subscribers.isEmpty) {
rx.sharingCancelable.cancel
rx.sharingCancelable = Cancelable.empty
}
}
case leaf: Var[A] =>
leaf.foreach(effect)
}
}
/** A smart variable that can be set manually. */
class Var[A](initialValue: Option[A], register: Var[A] => Cancelable) extends Rx[A] {
// Last computed value, retained to be sent to new subscribers as they come in.
private[mhtml] var cacheElem: Option[A] = initialValue
// Current registration to the feeding `Rx`, canceled whenever nobody's listening.
private[mhtml] var registration: Cancelable = Cancelable.empty
// Mutable set of all currently subscribed functions, implementing with an `Array`.
private[mhtml] val subscribers = buffer.empty[A => Unit]
// Is this Var currently imitating another Rx?
private[mhtml] var imitating = false
private[mhtml] def foreach(s: A => Unit): Cancelable = {
if (isCold) registration = register(this)
cacheElem match {
case Some(v) => s(v)
case None =>
}
subscribers += s
Cancelable { () =>
subscribers -= s
if (isCold) registration.cancel
}
}
/**
* Is there anything currently subscribed to this `Var`?
*
* This method is intended to be used to test the absence of memory leak.
* For instance, all `Var`s should be cold after canceling a `mount`.
*/
def isCold: Boolean = subscribers.isEmpty
/**
* Updates this `Var` with values emitted by the `other` `Rx`. This method
* is side effect free. Consequently, the returned `Rx` must be used at
* least once for the imitation to take place. This `Var` the `other` `Rx`
* and the returned `Rx` will all emit the same values.
*
* This method exists (only) to allow *circular dependency* in `Rx` graphs.
*/
def imitate(other: Rx[A]): Rx[A] = Rx.Imitate(this, other)
/** Sets the value of this `Var`. Triggers recalculation of depending `Rx`s. */
def :=(newValue: A): Unit = {
cacheElem = Some(newValue)
var i = subscribers.size
val copy = buffer[A => Unit](i)
while (i > 0) {
i = i - 1
val s = subscribers(i)
copy(i) = s
}
copy.foreach { f =>
f(newValue)
}
}
/** Updates the value of this `Var`. Triggers recalculation of depending `Rx`s. */
def update(f: A => A): Unit =
foreach(a => :=(f(a))).cancel
override def toString: String =
s"Var(${cacheElem.orNull})"
}
object Var {
/** Create a `Var` from an initial value. */
def apply[A](initialValue: A): Var[A] =
new Var[A](Some(initialValue), _ => Cancelable.empty)
/**
* Create a `Var` from an initial value and a register function .
*
* The register function is called as soon as this Var becomes active.
* The returned `Cancelable` is called when the Var becomes inactive.
*
* Registration & cancellation might append as many time as the Var goes
* active/inactive. This mechanism is what prevents `flatMap` from leaking
* memory: make sure that everything created in the register is canceled.
*/
def create[A](initialValue: A)(register: Var[A] => Cancelable): Var[A] =
new Var[A](Some(initialValue), register)
}
/** Action used to cancel subscription. */
final class Cancelable(val cancelFunction: () => Unit) extends AnyVal {
// scalac: side-effecting nullary methods are discouraged: suggest defining
// as `def cancel()` instead. Until we get systematic warnings for forgotten
// parenthesis, this will stay a side-effecting nullary method...
/** Cancel this subscription. */
def cancel: Unit = cancelFunction()
}
object Cancelable {
/** Creates a `Cancelable` with the specified cancel function. */
def apply(cancelFunction: () => Unit) = new Cancelable(cancelFunction)
/** The empty `Cancelable`. */
val empty = Cancelable(() => ())
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy