From 37acfb34354377fbf9882196544e05f99de1b6a4 Mon Sep 17 00:00:00 2001 From: Matt Hicks Date: Mon, 28 Oct 2019 21:27:51 -0500 Subject: [PATCH 1/6] Attempting to simplify the architecture and improve performance --- build.sbt | 2 +- .../src/main/scala/reactify/Reactive.scala | 23 ++- reactify/src/main/scala/reactify/State.scala | 145 ------------------ .../main/scala/reactify/StateCounter.scala | 39 ----- reactify/src/main/scala/reactify/Val.scala | 7 +- .../main/scala/reactify/group/ValGroup.scala | 4 +- .../main/scala/reactify/group/VarGroup.scala | 6 +- .../scala/reactify/standard/StandardVal.scala | 38 ++++- .../scala/reactify/standard/StandardVar.scala | 32 +--- reactify/src/test/scala/test/VarSpec.scala | 16 +- 10 files changed, 74 insertions(+), 238 deletions(-) delete mode 100644 reactify/src/main/scala/reactify/State.scala delete mode 100644 reactify/src/main/scala/reactify/StateCounter.scala diff --git a/build.sbt b/build.sbt index fad760a..c9b8f4e 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} name in ThisBuild := "reactify" organization in ThisBuild := "com.outr" -version in ThisBuild := "3.0.6-SNAPSHOT" +version in ThisBuild := "3.1.0-SNAPSHOT" scalaVersion in ThisBuild := "2.13.1" crossScalaVersions in ThisBuild := List("2.13.1", "2.12.8", "2.11.12") diff --git a/reactify/src/main/scala/reactify/Reactive.scala b/reactify/src/main/scala/reactify/Reactive.scala index b28dd5c..cf160eb 100644 --- a/reactify/src/main/scala/reactify/Reactive.scala +++ b/reactify/src/main/scala/reactify/Reactive.scala @@ -139,7 +139,28 @@ trait Reactive[T] { } object Reactive { + private val references: ThreadLocal[Option[Set[Val[_]]]] = new ThreadLocal[Option[Set[Val[_]]]] { + override def initialValue(): Option[Set[Val[_]]] = None + } + private[reactify] def fire[T](reactive: Reactive[T], value: T, previous: Option[T]): Unit = { reactive.fire(value, previous, reactive.reactions()) } -} \ No newline at end of file + + private[reactify] def processing[T](f: => T, mode: Var.Mode): FunctionResult[T] = { + val previous = references.get() + references.set(Some(Set.empty)) + try { + val value: T = f + new FunctionResult[T](() => f, mode, value, references.get().getOrElse(Set.empty)) + } finally { + references.set(previous) + } + } + + private[reactify] def getting[T](v: Val[T]): Unit = references.get().foreach { set => + references.set(Some(set + v)) + } +} + +class FunctionResult[T](val function: () => T, val mode: Var.Mode, val value: T, val referenced: Set[Val[_]]) \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/State.scala b/reactify/src/main/scala/reactify/State.scala deleted file mode 100644 index a5ad2fa..0000000 --- a/reactify/src/main/scala/reactify/State.scala +++ /dev/null @@ -1,145 +0,0 @@ -package reactify - -import reactify.reaction.{Reaction, ReactionStatus} - -/** - * State is an internal class to represent the assigned state of a `Val`, `Var`, or `Dep` - */ -case class State[T](owner: Reactive[T], index: Long, function: () => T) extends Reaction[Any] { - private var _previousState: Option[State[T]] = None - private var _nextState: Option[State[T]] = None - private var default: T = _ - private var _value: Option[T] = None - private var _references: List[State[_]] = Nil - - private lazy val updating = new ThreadLocal[Boolean] { - override def initialValue(): Boolean = false - } - - override def apply(value: Any, previous: Option[Any]): ReactionStatus = { - update() - ReactionStatus.Continue - } - - /** - * The previous state before this one - */ - def previousState: Option[State[T]] = _previousState - - /** - * The next state after this one if this is not active - */ - def nextState: Option[State[T]] = _nextState - - /** - * True if it is the currently active state - */ - def active: Boolean = nextState.isEmpty - - /** - * The currently active state - */ - def activeState: State[T] = nextState match { - case Some(next) => next.activeState - case None => this - } - - /** - * Currently cached value derived from state function - */ - def cached: Option[T] = _value - - /** - * Current value of this state - */ - def value: T = { - StateCounter.referenced(this) - updatingState match { - case Some(ps) => { - val previous = ps.value - previous - } - case None => _value.getOrElse(throw new RuntimeException("State.value has not been set yet!")) - } - } - - private def updatingState: Option[State[T]] = if (updating.get()) { - if (previousState.isEmpty) { - throw new RuntimeException(s"Invalid reference to recursive state with no previous value for ${_value}. This should only happen if the function doesn't always expose a reference to itself.") - } - previousState - } else { - previousState.flatMap(_.updatingState) - } - - /** - * All states referenced in the function deriving this state's value - */ - def references: List[State[_]] = _references - - /** - * Updates the derived value of this state - */ - def update(previous: Option[State[T]] = _previousState): Unit = synchronized { - if (!updating.get()) { - clearReferences() - if (previousState.nonEmpty && previous.isEmpty) throw new RuntimeException(s"Cannot remove previous state if set already!") - _previousState = previous - val (value, allReferences) = StateCounter.transaction { - updating.set(true) - try { - function() - } finally { - updating.set(false) - } - } - val references = allReferences.filterNot(_ == activeState) - val previousValue = _value.orElse(previous.map(_.value)).getOrElse(default) - val modified = previousValue != value - _value = Some(value) - val removed = _references.diff(references) - val added = references.diff(_references) - removed.foreach(removeReference) - added.foreach(addReference) - previous.foreach { previousState => - if (allReferences.contains(activeState)) { - this._previousState = Some(previousState) - } else { - previousState.clearReferences() - this._previousState = None - } - } - _references = references - if (modified && active) { - previous.foreach { p => - if (p ne this) { - p._nextState = Some(this) - } - } - Reactive.fire(owner, value, Some(previousValue)) - } else { - _nextState.foreach { n => - n.update() - } - } - } - } - - private def addReference(state: State[_]): Unit = { - state.owner.asInstanceOf[Reactive[Any]].reactions += this - } - - private def removeReference(state: State[_]): Unit = { - state.owner.asInstanceOf[Reactive[Any]].reactions -= this - } - - /** - * Clears all references to other states from this state - */ - def clearReferences(): Unit = synchronized { - references.foreach(removeReference) - _references = Nil - } - - override def toString: String = s"State(owner: $owner, index: $index, value: ${_value}, active: $active, hasPrevious: ${previousState.nonEmpty}, hasNext: ${nextState.nonEmpty})" -} \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/StateCounter.scala b/reactify/src/main/scala/reactify/StateCounter.scala deleted file mode 100644 index d720bf3..0000000 --- a/reactify/src/main/scala/reactify/StateCounter.scala +++ /dev/null @@ -1,39 +0,0 @@ -package reactify - -class StateCounter { - var references: List[State[_]] = Nil -} - -/** - * StateCounter provides infrastructure to glean the references to `State`s within a functional block of code. This is - * primarily for internal use, but can be used externally to get additional information regarding references. - */ -object StateCounter { - private val instance = new ThreadLocal[Option[StateCounter]] { - override def initialValue(): Option[StateCounter] = None - } - - /** - * Use this method to get a list of `State` references used by the underlying function block - */ - def transaction[Return](f: => Return): (Return, List[State[_]]) = { - val previous = instance.get() - val counter = new StateCounter - instance.set(Some(counter)) - try { - val value: Return = f - (value, counter.references) - } finally { - instance.set(previous) - } - } - - /** - * Called by a State when it is referenced to get the value - */ - def referenced(state: State[_]): Unit = { - instance.get().foreach { counter => - counter.references = state :: counter.references - } - } -} \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/Val.scala b/reactify/src/main/scala/reactify/Val.scala index 240c8ac..653988f 100644 --- a/reactify/src/main/scala/reactify/Val.scala +++ b/reactify/src/main/scala/reactify/Val.scala @@ -11,15 +11,10 @@ import reactify.standard.StandardVal * @tparam T the type of value this Reactive receives */ trait Val[T] extends Reactive[T] { - /** - * The current State representation - */ - def state: State[T] - /** * Gets the current value from the current `State` */ - def get: T = state.value + def get: T /** * Convenience wrapper around `get` diff --git a/reactify/src/main/scala/reactify/group/ValGroup.scala b/reactify/src/main/scala/reactify/group/ValGroup.scala index cfe4bfa..6a7618d 100644 --- a/reactify/src/main/scala/reactify/group/ValGroup.scala +++ b/reactify/src/main/scala/reactify/group/ValGroup.scala @@ -1,10 +1,10 @@ package reactify.group import reactify.reaction.{GroupReactions, Reactions} -import reactify.{State, Val} +import reactify.Val case class ValGroup[T](override val name: Option[String], items: List[Val[T]]) extends Val[T] with Group[T, Val[T]] { override lazy val reactions: Reactions[T] = new GroupReactions(this) - override def state: State[T] = ??? + override def get: T = items.head.get } \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/group/VarGroup.scala b/reactify/src/main/scala/reactify/group/VarGroup.scala index 589e45b..6e30117 100644 --- a/reactify/src/main/scala/reactify/group/VarGroup.scala +++ b/reactify/src/main/scala/reactify/group/VarGroup.scala @@ -1,7 +1,7 @@ package reactify.group import reactify.reaction.{GroupReactions, Reactions} -import reactify.{State, Var} +import reactify.Var case class VarGroup[T](override val name: Option[String], items: List[Var[T]]) extends Var[T] with Group[T, Var[T]] { override def mode: Var.Mode = items.head.mode @@ -12,7 +12,7 @@ case class VarGroup[T](override val name: Option[String], items: List[Var[T]]) e override def set(value: => T, mode: Var.Mode): Unit = items.foreach(_.set(value, mode)) - override def state: State[T] = ??? - override def and(that: Var[T]): Var[T] = VarGroup(name, items ::: List(that)) + + override def get: T = items.head.get } \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/standard/StandardVal.scala b/reactify/src/main/scala/reactify/standard/StandardVal.scala index 63afc58..f9f44d7 100644 --- a/reactify/src/main/scala/reactify/standard/StandardVal.scala +++ b/reactify/src/main/scala/reactify/standard/StandardVal.scala @@ -1,9 +1,39 @@ package reactify.standard -import reactify.{State, Val} +import reactify.reaction.{Reaction, ReactionStatus, Reactions} +import reactify.{FunctionResult, Reactive, Val, Var} class StandardVal[T](f: => T, override val name: Option[String]) extends Val[T] { - override val state: State[T] = new State[T](this, 1, () => f) + private var result: FunctionResult[T] = _ + refresh(f, mode) - state.update(None) -} + protected def mode: Var.Mode = Var.Mode.Normal + + private lazy val reaction: Reaction[Any] = new Reaction[Any] { + override def apply(value: Any, previous: Option[Any]): ReactionStatus = { + recalculate() + ReactionStatus.Continue + } + } + + protected def recalculate(): Unit = refresh(result.function(), result.mode) + + protected def refresh(f: => T, mode: Var.Mode): Unit = synchronized { + val previousValue = Option(result).map(_.value) + val previousReferences = Option(result).map(_.referenced).getOrElse(Set.empty) + mode match { + case Var.Mode.Normal => result = Reactive.processing(f, mode) + case Var.Mode.Static => result = new FunctionResult[T](() => f, mode, f, Set.empty) + } + val removed = previousReferences.diff(result.referenced) + val added = result.referenced.diff(previousReferences) + removed.foreach(_.reactions.asInstanceOf[Reactions[Any]] -= reaction) + added.foreach(_.reactions.asInstanceOf[Reactions[Any]] += reaction) + + if (!previousValue.contains(result.value)) { + fire(result.value, previousValue) + } + } + + override def get: T = result.value +} \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/standard/StandardVar.scala b/reactify/src/main/scala/reactify/standard/StandardVar.scala index b0944f6..c79d0de 100644 --- a/reactify/src/main/scala/reactify/standard/StandardVar.scala +++ b/reactify/src/main/scala/reactify/standard/StandardVar.scala @@ -1,35 +1,9 @@ package reactify.standard -import java.util.concurrent.atomic.AtomicLong - -import reactify.transaction.Transaction -import reactify.{State, Var} - -class StandardVar[T](f: => T, override val mode: Var.Mode, override val name: Option[String]) extends Var[T] { - private lazy val counter = new AtomicLong(0L) - - private var _state: State[T] = new State[T](this, counter.incrementAndGet(), () => f) - - _state.update() - - override def state: State[T] = _state +import reactify.Var +class StandardVar[T](f: => T, override val mode: Var.Mode, name: Option[String]) extends StandardVal[T](f, name) with Var[T] { override def set(value: => T): Unit = set(value, mode) - override def set(value: => T, mode: Var.Mode): Unit = synchronized { - val previous = _state - mode match { - case Var.Mode.Normal => { - _state = new State[T](this, counter.incrementAndGet(), () => value) - _state.update(Some(previous)) - Transaction.change(this, previous.function, _state.function) - } - case Var.Mode.Static => { - val staticValue: T = value - _state = new State[T](this, counter.incrementAndGet(), () => staticValue) - _state.update(None) - Transaction.change(this, previous.function, _state.function) - } - } - } + override def set(value: => T, mode: Var.Mode): Unit = refresh(value, mode) } \ No newline at end of file diff --git a/reactify/src/test/scala/test/VarSpec.scala b/reactify/src/test/scala/test/VarSpec.scala index 021b27a..69f267f 100644 --- a/reactify/src/test/scala/test/VarSpec.scala +++ b/reactify/src/test/scala/test/VarSpec.scala @@ -31,8 +31,8 @@ class VarSpec extends WordSpec with Matchers { val v2 = Var(s"Hello, ${v1()}") v1.reactions().size should be(1) v2.reactions().size should be(0) - v2.state.references.size should be(1) - v2.state.references should be(List(v1.state)) +// v2.state.references.size should be(1) +// v2.state.references should be(List(v1.state)) v2.on(changed += 1) v2.reactions().size should be(1) v2() should be("Hello, Matt") @@ -135,7 +135,7 @@ class VarSpec extends WordSpec with Matchers { v := v * 4 v() should be(12) } - "derive a value from itself depending on another value" in { + /*"derive a value from itself depending on another value" in { val v1 = Var(1) val v2 = Var(v1 + 1) @@ -175,7 +175,7 @@ class VarSpec extends WordSpec with Matchers { v2State2.nextState should be(None) v2() should be(6) - } + }*/ "create a variable that builds upon itself multiple times" in { val v = Var(1) v := v + v + v @@ -187,11 +187,11 @@ class VarSpec extends WordSpec with Matchers { val list = Var(List.empty[String], name = Some("list")) list := s1() :: s2() :: list() list() should be(List("One", "Two")) - list.state.index should be(2) - list.state.references.toSet should be(Set(s1.state, s2.state, list.state.previousState.get)) - s2.reactions() should contain(list.state) +// list.state.index should be(2) +// list.state.references.toSet should be(Set(s1.state, s2.state, list.state.previousState.get)) +// s2.reactions() should contain(list.state) s2 := "Three" - list.state.index should be(2) +// list.state.index should be(2) list() should be(List("One", "Three")) s1 := "Two" list() should be(List("Two", "Three")) From 750de3605332d4c61a8c2be984adf4aa827d15c4 Mon Sep 17 00:00:00 2001 From: Matt Hicks Date: Thu, 14 Nov 2019 19:41:51 -0600 Subject: [PATCH 2/6] Not working! --- .../src/main/scala/reactify/Reactive.scala | 31 +++++++++++++------ .../scala/reactify/standard/StandardVal.scala | 21 +++++++++---- .../scala/reactify/standard/StandardVar.scala | 2 +- reactify/src/test/scala/test/VarSpec.scala | 14 ++++----- 4 files changed, 44 insertions(+), 24 deletions(-) diff --git a/reactify/src/main/scala/reactify/Reactive.scala b/reactify/src/main/scala/reactify/Reactive.scala index cf160eb..8e6b8da 100644 --- a/reactify/src/main/scala/reactify/Reactive.scala +++ b/reactify/src/main/scala/reactify/Reactive.scala @@ -139,28 +139,39 @@ trait Reactive[T] { } object Reactive { - private val references: ThreadLocal[Option[Set[Val[_]]]] = new ThreadLocal[Option[Set[Val[_]]]] { - override def initialValue(): Option[Set[Val[_]]] = None + private val referenced: ThreadLocal[Option[FunctionReferenced]] = new ThreadLocal[Option[FunctionReferenced]] { + override def initialValue(): Option[FunctionReferenced] = None } private[reactify] def fire[T](reactive: Reactive[T], value: T, previous: Option[T]): Unit = { reactive.fire(value, previous, reactive.reactions()) } - private[reactify] def processing[T](f: => T, mode: Var.Mode): FunctionResult[T] = { - val previous = references.get() - references.set(Some(Set.empty)) + private[reactify] def processing[T](v: Val[T], f: => T, mode: Var.Mode, previous: T): FunctionResult[T] = { + val p = referenced.get() + referenced.set(Some(new FunctionReferenced(previous, v, Set.empty))) try { val value: T = f - new FunctionResult[T](() => f, mode, value, references.get().getOrElse(Set.empty)) + val references = referenced.get().map(_.referenced).getOrElse(Set.empty) + new FunctionResult[T](() => f, mode, value, previous, references) } finally { - references.set(previous) + referenced.set(p) } } - private[reactify] def getting[T](v: Val[T]): Unit = references.get().foreach { set => - references.set(Some(set + v)) + private[reactify] def getting[T](v: Val[T]): Option[T] = referenced.get().flatMap { r => + println("GETTING!") + if (r.processing ne v) { + referenced.set(Some(r.withReference(v))) + None + } else { + Some(r.previous.asInstanceOf[T]) + } } } -class FunctionResult[T](val function: () => T, val mode: Var.Mode, val value: T, val referenced: Set[Val[_]]) \ No newline at end of file +class FunctionReferenced(val previous: Any, val processing: Val[_], val referenced: Set[Val[_]]) { + def withReference(v: Val[_]): FunctionReferenced = new FunctionReferenced(previous, processing, referenced + v) +} + +class FunctionResult[T](val function: () => T, val mode: Var.Mode, val value: T, val previous: T, val referenced: Set[Val[_]]) \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/standard/StandardVal.scala b/reactify/src/main/scala/reactify/standard/StandardVal.scala index f9f44d7..c435156 100644 --- a/reactify/src/main/scala/reactify/standard/StandardVal.scala +++ b/reactify/src/main/scala/reactify/standard/StandardVal.scala @@ -5,25 +5,34 @@ import reactify.{FunctionResult, Reactive, Val, Var} class StandardVal[T](f: => T, override val name: Option[String]) extends Val[T] { private var result: FunctionResult[T] = _ - refresh(f, mode) + refresh(f, mode, replacing = true) protected def mode: Var.Mode = Var.Mode.Normal private lazy val reaction: Reaction[Any] = new Reaction[Any] { override def apply(value: Any, previous: Option[Any]): ReactionStatus = { + println(s"reaction recalculate: $value, previous: $previous") recalculate() ReactionStatus.Continue } } - protected def recalculate(): Unit = refresh(result.function(), result.mode) + protected def recalculate(): Unit = { + println("Recalculating!") + refresh(result.function(), result.mode, replacing = false) + } - protected def refresh(f: => T, mode: Var.Mode): Unit = synchronized { + protected def refresh(f: => T, mode: Var.Mode, replacing: Boolean): Unit = synchronized { val previousValue = Option(result).map(_.value) val previousReferences = Option(result).map(_.referenced).getOrElse(Set.empty) + val previous = if (replacing) { + previousValue.getOrElse(null.asInstanceOf[T]) + } else { + Option(result).map(_.previous).getOrElse(null.asInstanceOf[T]) + } mode match { - case Var.Mode.Normal => result = Reactive.processing(f, mode) - case Var.Mode.Static => result = new FunctionResult[T](() => f, mode, f, Set.empty) + case Var.Mode.Normal => result = Reactive.processing(this, f, mode, previous) + case Var.Mode.Static => result = new FunctionResult[T](() => f, mode, f, previous, Set.empty) } val removed = previousReferences.diff(result.referenced) val added = result.referenced.diff(previousReferences) @@ -35,5 +44,5 @@ class StandardVal[T](f: => T, override val name: Option[String]) extends Val[T] } } - override def get: T = result.value + override def get: T = Reactive.getting(this).getOrElse(result.value) } \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/standard/StandardVar.scala b/reactify/src/main/scala/reactify/standard/StandardVar.scala index c79d0de..11b4202 100644 --- a/reactify/src/main/scala/reactify/standard/StandardVar.scala +++ b/reactify/src/main/scala/reactify/standard/StandardVar.scala @@ -5,5 +5,5 @@ import reactify.Var class StandardVar[T](f: => T, override val mode: Var.Mode, name: Option[String]) extends StandardVal[T](f, name) with Var[T] { override def set(value: => T): Unit = set(value, mode) - override def set(value: => T, mode: Var.Mode): Unit = refresh(value, mode) + override def set(value: => T, mode: Var.Mode): Unit = refresh(value, mode, replacing = true) } \ No newline at end of file diff --git a/reactify/src/test/scala/test/VarSpec.scala b/reactify/src/test/scala/test/VarSpec.scala index 69f267f..785a150 100644 --- a/reactify/src/test/scala/test/VarSpec.scala +++ b/reactify/src/test/scala/test/VarSpec.scala @@ -185,7 +185,7 @@ class VarSpec extends WordSpec with Matchers { val s1 = Var("One", name = Some("s1")) val s2 = Var("Two", name = Some("s2")) val list = Var(List.empty[String], name = Some("list")) - list := s1() :: s2() :: list() + list := s1() :: s2() :: Nil list() should be(List("One", "Two")) // list.state.index should be(2) // list.state.references.toSet should be(Set(s1.state, s2.state, list.state.previousState.get)) @@ -195,12 +195,12 @@ class VarSpec extends WordSpec with Matchers { list() should be(List("One", "Three")) s1 := "Two" list() should be(List("Two", "Three")) - list := "One" :: list() - list() should be(List("One", "Two", "Three")) - s2 := "Four" - list() should be(List("One", "Two", "Four")) +// list := "One" :: list() +// list() should be(List("One", "Two", "Three")) +// s2 := "Four" +// list() should be(List("One", "Two", "Four")) } - "create a Container with a generic Child list" in { + /*"create a Container with a generic Child list" in { val v1 = Var("One") val v2 = Var("Two") val container = new Container[String] @@ -411,7 +411,7 @@ class VarSpec extends WordSpec with Matchers { val v = Var[Double](lazyDouble) lazyDouble := 100.0 v() should be(100.0) - } + }*/ } class Container[Child] { From 151bb2654b600803ce849ff0ea6e439ad43edb52 Mon Sep 17 00:00:00 2001 From: Matt Hicks Date: Wed, 18 Mar 2020 16:05:29 -0500 Subject: [PATCH 3/6] Began complete re-write again with a focus on greater simplicity and reasonable recursive support --- build.sbt | 4 +- project/build.properties | 2 +- project/plugins.sbt | 18 ++-- .../main/scala/reactify/prototype/Test.scala | 92 +++++++++++++++++++ .../src/test/scala/test/BindingSpec.scala | 5 +- .../src/test/scala/test/ChannelSpec.scala | 5 +- reactify/src/test/scala/test/DepSpec.scala | 5 +- .../src/test/scala/test/DepSpecialSpec.scala | 5 +- .../src/test/scala/test/TransactionSpec.scala | 5 +- .../src/test/scala/test/TriggerSpec.scala | 5 +- reactify/src/test/scala/test/ValSpec.scala | 5 +- reactify/src/test/scala/test/VarSpec.scala | 5 +- 12 files changed, 127 insertions(+), 29 deletions(-) create mode 100644 reactify/src/main/scala/reactify/prototype/Test.scala diff --git a/build.sbt b/build.sbt index c9b8f4e..efe4f0d 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} name in ThisBuild := "reactify" organization in ThisBuild := "com.outr" -version in ThisBuild := "3.1.0-SNAPSHOT" +version in ThisBuild := "4.0.0-SNAPSHOT" scalaVersion in ThisBuild := "2.13.1" crossScalaVersions in ThisBuild := List("2.13.1", "2.12.8", "2.11.12") @@ -22,7 +22,7 @@ developers in ThisBuild := List( Developer(id="darkfrog", name="Matt Hicks", email="matt@matthicks.", url=url("http://matthicks.com")) ) -val scalatestVersion = "3.1.0-SNAP13" +val scalatestVersion = "3.2.0-M3" lazy val reactify = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) diff --git a/project/build.properties b/project/build.properties index c0bab04..a919a9b 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.2.8 +sbt.version=1.3.8 diff --git a/project/plugins.sbt b/project/plugins.sbt index c992d89..353d9aa 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,12 +1,10 @@ -resolvers += "Artima Maven Repository" at "http://repo.artima.com/releases" -resolvers += "Typesafe Repository" at "https://repo.typesafe.com/typesafe/releases/" +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1") +addSbtPlugin("com.codacy" % "sbt-codacy-coverage" % "3.0.3") -addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.0") -addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "0.6.0") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.3.8") +addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.8.1") +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.1") -addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3") -addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.2-1") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") -addSbtPlugin("com.codacy" % "sbt-codacy-coverage" % "1.3.15") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0") +addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.0.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.0.1") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.0-M2") \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/prototype/Test.scala b/reactify/src/main/scala/reactify/prototype/Test.scala new file mode 100644 index 0000000..3d3dec9 --- /dev/null +++ b/reactify/src/main/scala/reactify/prototype/Test.scala @@ -0,0 +1,92 @@ +package reactify.prototype + +object Test { + def main(args: Array[String]): Unit = { + val v = Var(1) + println(s"Value: ${v.get}") + v.set(2) + println(s"Value: ${v.get}") + v.set(3 + v.get) + + val v2 = Var(v.get + 5) + v := 6 + } +} + +class Var[T] private() { + private var function: () => T = _ + private var evaluated: T = _ + private var previous: Option[T] = None + private var references: Set[Var[_]] = Set.empty + + def set(value: => T): Unit = { + function = () => value + Var.evaluate(this, updating = false) + } + + def static(value: T): Unit = { + function = () => value + previous = Option(evaluated) + evaluated = value + references = Set.empty + } + + def get: T = Var.get(this) + + def :=(value: => T): Unit = set(value) + def @=(value: T): Unit = static(value) + def apply(): T = get + + def equality(t1: T, t2: T): Boolean = t1 == t2 + + override def toString: String = s"Var($evaluated)" +} + +object Var { + private val evaluating: ThreadLocal[Option[Evaluating]] = new ThreadLocal[Option[Evaluating]] { + override def initialValue(): Option[Evaluating] = None + } + + def apply[T](value: => T): Var[T] = { + val v = new Var[T] + v.set(value) + v + } + + def evaluate[T](v: Var[T], updating: Boolean): Unit = { + assert(evaluating.get().isEmpty, s"Expected empty evaluating, but found: ${evaluating.get()}") + val e = new Evaluating(v, updating) + evaluating.set(Some(e)) + val evaluated = try { + v.function() + } finally { + evaluating.set(None) + } + if (evaluated != v.evaluated) { + v.previous = Option(v.evaluated) + v.evaluated = evaluated + // TODO: Fire + println(s"Modified! Previous: ${v.previous}, Current: $evaluated, References: ${e.references}") + } + } + + def get[T](v: Var[T]): T = { + evaluating.get() match { + case Some(e) if e.v eq v => { + if (e.updating) { + v.previous.getOrElse(throw new RuntimeException("Attempting to get previous on None!")) + } else { + v.evaluated + } + } + case Some(e) => { + e.references += v + + v.evaluated + } + case None => v.evaluated + } + } +} + +class Evaluating(val v: Var[_], val updating: Boolean, var references: Set[Var[_]] = Set.empty) \ No newline at end of file diff --git a/reactify/src/test/scala/test/BindingSpec.scala b/reactify/src/test/scala/test/BindingSpec.scala index 04d10f6..4370306 100644 --- a/reactify/src/test/scala/test/BindingSpec.scala +++ b/reactify/src/test/scala/test/BindingSpec.scala @@ -1,10 +1,11 @@ package test -import org.scalatest.{Matchers, WordSpec} +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec import reactify._ import reactify.bind.Binding -class BindingSpec extends WordSpec with Matchers { +class BindingSpec extends AnyWordSpec with Matchers { "Bindings" when { "dealing with a simple binding" should { val a = Var[String]("a") diff --git a/reactify/src/test/scala/test/ChannelSpec.scala b/reactify/src/test/scala/test/ChannelSpec.scala index 46fdfeb..56ffd63 100644 --- a/reactify/src/test/scala/test/ChannelSpec.scala +++ b/reactify/src/test/scala/test/ChannelSpec.scala @@ -1,9 +1,10 @@ package test -import org.scalatest.{Matchers, WordSpec} +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec import reactify.Channel -class ChannelSpec extends WordSpec with Matchers { +class ChannelSpec extends AnyWordSpec with Matchers { "Channels" should { "notify when changed" in { var changes = 0 diff --git a/reactify/src/test/scala/test/DepSpec.scala b/reactify/src/test/scala/test/DepSpec.scala index 69a46b2..f8c21eb 100644 --- a/reactify/src/test/scala/test/DepSpec.scala +++ b/reactify/src/test/scala/test/DepSpec.scala @@ -1,11 +1,12 @@ package test -import org.scalatest.{Matchers, WordSpec} +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec import reactify._ import scala.collection.mutable.ListBuffer -class DepSpec extends WordSpec with Matchers { +class DepSpec extends AnyWordSpec with Matchers { "Deps" should { val width: Var[Double] = Var(0.0) diff --git a/reactify/src/test/scala/test/DepSpecialSpec.scala b/reactify/src/test/scala/test/DepSpecialSpec.scala index 800af64..2d3aa81 100644 --- a/reactify/src/test/scala/test/DepSpecialSpec.scala +++ b/reactify/src/test/scala/test/DepSpecialSpec.scala @@ -1,9 +1,10 @@ package test -import org.scalatest.{Matchers, WordSpec} +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec import reactify._ -class DepSpecialSpec extends WordSpec with Matchers { +class DepSpecialSpec extends AnyWordSpec with Matchers { "Deps Special Use-Cases" when { "combining Ints" should { "do simple addition" in { diff --git a/reactify/src/test/scala/test/TransactionSpec.scala b/reactify/src/test/scala/test/TransactionSpec.scala index 99d3552..6f44e58 100644 --- a/reactify/src/test/scala/test/TransactionSpec.scala +++ b/reactify/src/test/scala/test/TransactionSpec.scala @@ -1,10 +1,11 @@ package test -import org.scalatest.{Matchers, WordSpec} +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec import reactify.Var import reactify.transaction.Transaction -class TransactionSpec extends WordSpec with Matchers { +class TransactionSpec extends AnyWordSpec with Matchers { "Transactions" should { "support undoing" in { val v = Var("One") diff --git a/reactify/src/test/scala/test/TriggerSpec.scala b/reactify/src/test/scala/test/TriggerSpec.scala index 7f34eff..23e98fa 100644 --- a/reactify/src/test/scala/test/TriggerSpec.scala +++ b/reactify/src/test/scala/test/TriggerSpec.scala @@ -1,9 +1,10 @@ package test -import org.scalatest.{Matchers, WordSpec} +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec import reactify.Trigger -class TriggerSpec extends WordSpec with Matchers { +class TriggerSpec extends AnyWordSpec with Matchers { "Triggers" should { "handle simple invocations" in { val t = Trigger() diff --git a/reactify/src/test/scala/test/ValSpec.scala b/reactify/src/test/scala/test/ValSpec.scala index 73107a2..7cca7eb 100644 --- a/reactify/src/test/scala/test/ValSpec.scala +++ b/reactify/src/test/scala/test/ValSpec.scala @@ -1,9 +1,10 @@ package test -import org.scalatest.{Matchers, WordSpec} +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec import reactify._ -class ValSpec extends WordSpec with Matchers { +class ValSpec extends AnyWordSpec with Matchers { "Vals" should { "contain the proper value" in { val v = Val(5) diff --git a/reactify/src/test/scala/test/VarSpec.scala b/reactify/src/test/scala/test/VarSpec.scala index 785a150..42fc315 100644 --- a/reactify/src/test/scala/test/VarSpec.scala +++ b/reactify/src/test/scala/test/VarSpec.scala @@ -1,12 +1,13 @@ package test -import org.scalatest.{Matchers, WordSpec} +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec import reactify._ import reactify.group.VarGroup import scala.collection.mutable.ListBuffer -class VarSpec extends WordSpec with Matchers { +class VarSpec extends AnyWordSpec with Matchers { lazy val lazyDouble: Var[Double] = Var(0.0) "Vars" should { From 53233e3d0f67b2314434e565765b1bedc98fe0c1 Mon Sep 17 00:00:00 2001 From: Matt Hicks Date: Thu, 19 Mar 2020 09:03:23 -0500 Subject: [PATCH 4/6] Began complete re-write again with a focus on greater simplicity and reasonable recursive support --- .../src/main/scala/reactify/Channel.scala | 48 +----- reactify/src/main/scala/reactify/Dep.scala | 24 ++- .../src/main/scala/reactify/Mutable.scala | 18 ++ .../src/main/scala/reactify/Priority.scala | 2 +- .../src/main/scala/reactify/Reactive.scala | 47 +----- .../src/main/scala/reactify/Stateful.scala | 39 +++++ .../src/main/scala/reactify/Trigger.scala | 7 +- reactify/src/main/scala/reactify/Val.scala | 129 ++++++++++----- reactify/src/main/scala/reactify/Var.scala | 57 ++----- .../scala/reactify/group/ChannelGroup.scala | 7 +- .../{reaction => group}/GroupReactions.scala | 9 +- .../scala/reactify/group/StatefulGroup.scala | 10 ++ .../main/scala/reactify/group/ValGroup.scala | 10 +- .../main/scala/reactify/group/VarGroup.scala | 10 +- .../main/scala/reactify/prototype/Test.scala | 92 ----------- .../reaction/ChangeFunctionReaction.scala | 2 +- .../reactify/reaction/FunctionReaction.scala | 2 +- .../scala/reactify/reaction/Reaction.scala | 4 +- .../scala/reactify/reaction/Reactions.scala | 23 ++- .../reactify/standard/StandardChannel.scala | 7 - .../scala/reactify/standard/StandardDep.scala | 18 -- .../reactify/standard/StandardReactions.scala | 24 --- .../scala/reactify/standard/StandardVal.scala | 48 ------ .../scala/reactify/standard/StandardVar.scala | 9 - .../reactify/transaction/Transaction.scala | 156 ------------------ .../transaction/TransactionChange.scala | 9 - .../src/test/scala/test/BindingSpec.scala | 2 +- reactify/src/test/scala/test/DepSpec.scala | 10 +- .../src/test/scala/test/DepSpecialSpec.scala | 24 +-- .../src/test/scala/test/TransactionSpec.scala | 2 + reactify/src/test/scala/test/VarSpec.scala | 6 +- 31 files changed, 253 insertions(+), 602 deletions(-) create mode 100644 reactify/src/main/scala/reactify/Mutable.scala create mode 100644 reactify/src/main/scala/reactify/Stateful.scala rename reactify/src/main/scala/reactify/{reaction => group}/GroupReactions.scala (79%) create mode 100644 reactify/src/main/scala/reactify/group/StatefulGroup.scala delete mode 100644 reactify/src/main/scala/reactify/prototype/Test.scala delete mode 100644 reactify/src/main/scala/reactify/standard/StandardChannel.scala delete mode 100644 reactify/src/main/scala/reactify/standard/StandardDep.scala delete mode 100644 reactify/src/main/scala/reactify/standard/StandardReactions.scala delete mode 100644 reactify/src/main/scala/reactify/standard/StandardVal.scala delete mode 100644 reactify/src/main/scala/reactify/standard/StandardVar.scala delete mode 100644 reactify/src/main/scala/reactify/transaction/Transaction.scala delete mode 100644 reactify/src/main/scala/reactify/transaction/TransactionChange.scala diff --git a/reactify/src/main/scala/reactify/Channel.scala b/reactify/src/main/scala/reactify/Channel.scala index b8f5388..0fc73b2 100644 --- a/reactify/src/main/scala/reactify/Channel.scala +++ b/reactify/src/main/scala/reactify/Channel.scala @@ -1,46 +1,9 @@ package reactify import reactify.group.ChannelGroup -import reactify.standard.StandardChannel -import scala.concurrent.Future -import scala.concurrent.ExecutionContext.Implicits.global - -/** - * Channel is a stateless Reactive implementation exposing a public method to fire values. - * - * @tparam T the type of value this Reactive receives - */ -trait Channel[T] extends Reactive[T] { - /** - * Public method to fire a value against the Reactions attached to this Channel - * - * @param value the function value - */ - def set(value: => T): Unit - - /** - * Convenience method to fire a value - * - * @see #set - * @param value the function value - */ - def :=(value: => T): Unit = set(value) - - /** - * Convenience method for static (non-functional) invocation. - * - * @see #set - * @param value the value - */ - def @=(value: T): Unit = set(value) - - /** - * Convenience functionality to assign the result of a future (upon completion) to this Channel - */ - def !(future: Future[T]): Future[Unit] = future.map { value => - set(value) - } +class Channel[T] extends Reactive[T] with Mutable[T] { + override def set(f: => T): Unit = fire(f, None, reactions()) /** * Group multiple channels together @@ -50,7 +13,7 @@ trait Channel[T] extends Reactive[T] { /** * Group multiple channels together */ - def and(that: Channel[T]): Channel[T] = ChannelGroup(None, List(this, that)) + def and(that: Channel[T]): Channel[T] = ChannelGroup(List(this, that)) /** * Functional mapping of this Channel into another Channel. All values received by this Channel will be mapped and @@ -82,11 +45,8 @@ trait Channel[T] extends Reactive[T] { } channel } - - override def toString: String = name.getOrElse("Channel") } object Channel { - def apply[T]: Channel[T] = new StandardChannel[T](None) - def apply[T](name: Option[String]): Channel[T] = new StandardChannel[T](name) + def apply[T]: Channel[T] = new Channel[T] } \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/Dep.scala b/reactify/src/main/scala/reactify/Dep.scala index eda66d3..2a0a2ca 100644 --- a/reactify/src/main/scala/reactify/Dep.scala +++ b/reactify/src/main/scala/reactify/Dep.scala @@ -1,6 +1,6 @@ package reactify -import reactify.standard.StandardDep +import reactify.reaction.{Reaction, ReactionStatus} /** * Dep allows creation of a dependent `Var` on another `Var` allowing conversion between the two. This can be useful for @@ -21,16 +21,22 @@ import reactify.standard.StandardDep * @tparam T the type of value this Reactive receives * @tparam R the type that this Dep receives */ -trait Dep[T, R] extends Var[T] { - def owner: Var[R] - def t2R(t: T): R - def r2T(r: R): T +class Dep[T, R] protected(val owner: Var[R], t2R: T => R, r2T: R => T) extends Reactive[T] with Stateful[T] with Mutable[T] { + private val v: Val[T] = Val(r2T(owner)) + + v.reactions += new Reaction[T] { + override def apply(value: T, previous: Option[T]): ReactionStatus = { + fire(value, previous, reactions()) + ReactionStatus.Continue + } + } + + override def get: T = v.get + + override def set(f: => T): Unit = owner := t2R(f) } object Dep { def apply[T, R](owner: Var[R]) - (implicit r2T: R => T, t2R: T => R): Dep[T, R] = new StandardDep[T, R](None, owner, r2T, t2R) - def apply[T, R](owner: Var[R], - name: String) - (implicit r2T: R => T, t2R: T => R): Dep[T, R] = new StandardDep[T, R](Option(name), owner, r2T, t2R) + (implicit r2T: R => T, t2R: T => R): Dep[T, R] = new Dep[T, R](owner, t2R, r2T) } \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/Mutable.scala b/reactify/src/main/scala/reactify/Mutable.scala new file mode 100644 index 0000000..4b32c3a --- /dev/null +++ b/reactify/src/main/scala/reactify/Mutable.scala @@ -0,0 +1,18 @@ +package reactify + +import scala.concurrent.{ExecutionContext, Future} + +trait Mutable[T] { + def set(f: => T): Unit + def static(f: T): Unit = set(f) + + def :=(f: => T): Unit = set(f) + def @=(f: T): Unit = static(f) + + /** + * Convenience functionality to assign the result of a future (upon completion) to this Channel + */ + def !(future: Future[T])(implicit ec: ExecutionContext): Future[Unit] = future.map { value => + set(value) + } +} \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/Priority.scala b/reactify/src/main/scala/reactify/Priority.scala index 5a95251..4dca8c3 100644 --- a/reactify/src/main/scala/reactify/Priority.scala +++ b/reactify/src/main/scala/reactify/Priority.scala @@ -7,6 +7,6 @@ object Priority { val Lowest: Double = Double.MinValue val Low: Double = -100.0 val Normal: Double = 0.0 - val High: Double = 100.0D + val High: Double = 100.0 val Highest: Double = Double.MaxValue } diff --git a/reactify/src/main/scala/reactify/Reactive.scala b/reactify/src/main/scala/reactify/Reactive.scala index 8e6b8da..0e2bfb1 100644 --- a/reactify/src/main/scala/reactify/Reactive.scala +++ b/reactify/src/main/scala/reactify/Reactive.scala @@ -1,23 +1,17 @@ package reactify import reactify.reaction.{Reaction, ReactionStatus, Reactions} -import reactify.standard.StandardReactions import scala.annotation.tailrec import scala.concurrent.{Future, Promise} /** * Reactive is the core trait for Reactify. The basic premise is that a Reactive represents an instance that can attach - * Reactions and fire instances of `T` that are received by those Reactions. + * Reactions and fire `T` and are received by those Reactions. * * @tparam T the type of value this Reactive receives */ trait Reactive[T] { - /** - * An optional name associated. This is primarily used for distinguishing between instances as well as logging. - */ - def name: Option[String] = None - private lazy val _status = new ThreadLocal[Option[ReactionStatus]] { override def initialValue(): Option[ReactionStatus] = None } @@ -40,7 +34,7 @@ trait Reactive[T] { /** * Reactions currently associated with this Reactive */ - lazy val reactions: Reactions[T] = new StandardReactions[T] + lazy val reactions: Reactions[T] = new Reactions[T] /** * Convenience method to create a Reaction to attach to this Reactive @@ -138,40 +132,3 @@ trait Reactive[T] { } } -object Reactive { - private val referenced: ThreadLocal[Option[FunctionReferenced]] = new ThreadLocal[Option[FunctionReferenced]] { - override def initialValue(): Option[FunctionReferenced] = None - } - - private[reactify] def fire[T](reactive: Reactive[T], value: T, previous: Option[T]): Unit = { - reactive.fire(value, previous, reactive.reactions()) - } - - private[reactify] def processing[T](v: Val[T], f: => T, mode: Var.Mode, previous: T): FunctionResult[T] = { - val p = referenced.get() - referenced.set(Some(new FunctionReferenced(previous, v, Set.empty))) - try { - val value: T = f - val references = referenced.get().map(_.referenced).getOrElse(Set.empty) - new FunctionResult[T](() => f, mode, value, previous, references) - } finally { - referenced.set(p) - } - } - - private[reactify] def getting[T](v: Val[T]): Option[T] = referenced.get().flatMap { r => - println("GETTING!") - if (r.processing ne v) { - referenced.set(Some(r.withReference(v))) - None - } else { - Some(r.previous.asInstanceOf[T]) - } - } -} - -class FunctionReferenced(val previous: Any, val processing: Val[_], val referenced: Set[Val[_]]) { - def withReference(v: Val[_]): FunctionReferenced = new FunctionReferenced(previous, processing, referenced + v) -} - -class FunctionResult[T](val function: () => T, val mode: Var.Mode, val value: T, val previous: T, val referenced: Set[Val[_]]) \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/Stateful.scala b/reactify/src/main/scala/reactify/Stateful.scala new file mode 100644 index 0000000..06c2cf9 --- /dev/null +++ b/reactify/src/main/scala/reactify/Stateful.scala @@ -0,0 +1,39 @@ +package reactify + +import reactify.group.StatefulGroup +import reactify.reaction.Reaction + +trait Stateful[T] extends Reactive[T] { + /** + * Gets the current value from the current `State` + */ + def get: T + + /** + * Convenience wrapper around `get` + */ + def apply(): T = get + + /** + * Convenience functionality to attach a Reaction and immediately fire the current state on the Reaction. + * + * @param f the function reaction + * @param priority the priority in comparison to other reactions (Defaults to Priority.Normal) + * @return Reaction[T] + */ + def attachAndFire(f: T => Unit, priority: Double = Priority.Normal): Reaction[T] = { + val reaction = attach(f, priority) + fire(get, Some(get), List(reaction)) + reaction + } + + /** + * Group multiple Vals together + */ + def &(that: Stateful[T]): Stateful[T] = and(that) + + /** + * Group multiple Vals together + */ + def and(that: Stateful[T]): Stateful[T] = StatefulGroup[T](List(this, that)) +} \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/Trigger.scala b/reactify/src/main/scala/reactify/Trigger.scala index a06142c..ec9f87b 100644 --- a/reactify/src/main/scala/reactify/Trigger.scala +++ b/reactify/src/main/scala/reactify/Trigger.scala @@ -1,16 +1,13 @@ package reactify -import reactify.standard.StandardChannel - /** * Trigger is a convenience class wrapping `Channel[Unit]` specifically for scenarios where the value doesn't matter, * just the reactions themselves. */ -trait Trigger extends Channel[Unit] { +class Trigger extends Channel[Unit] { def trigger(): Unit = fire((), None) } object Trigger { - def apply(): Trigger = new StandardChannel[Unit](None) with Trigger - def apply(name: String): Trigger = new StandardChannel[Unit](Option(name)) with Trigger + def apply(): Trigger = new Trigger } \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/Val.scala b/reactify/src/main/scala/reactify/Val.scala index 653988f..ff001a0 100644 --- a/reactify/src/main/scala/reactify/Val.scala +++ b/reactify/src/main/scala/reactify/Val.scala @@ -1,38 +1,45 @@ package reactify import reactify.group.ValGroup -import reactify.reaction.Reaction -import reactify.standard.StandardVal - -/** - * Val represents a final variable that cannot be set apart from its instantiation. However, unlike a Scala `val`, a - * `Val` may still fire changes if its value is derived from `Var`s that make it up. A `Val` is a stateful `Reactive`. - * - * @tparam T the type of value this Reactive receives - */ -trait Val[T] extends Reactive[T] { - /** - * Gets the current value from the current `State` - */ - def get: T +import reactify.reaction.{Reaction, ReactionStatus, Reactions} - /** - * Convenience wrapper around `get` - */ - def apply(): T = get +class Val[T] protected() extends Reactive[T] with Stateful[T] { + protected def this(f: => T) = { + this() + + set(f) + } + + protected var function: () => T = _ + protected var evaluated: T = _ + protected var previous: Option[T] = None + protected var _references: Set[Val[_]] = Set.empty + + def references: Set[Val[_]] = _references + + private lazy val reaction: Reaction[Any] = new Reaction[Any] { + override def apply(value: Any, previous: Option[Any]): ReactionStatus = { + Val.evaluate(Val.this, updating = true) + ReactionStatus.Continue + } + } + + protected def set(value: => T): Unit = { + function = () => value + Val.evaluate(this, updating = false) + } + + def static(value: T): Unit = { + function = () => value + previous = Option(evaluated) + evaluated = value + _references = Set.empty + } /** - * Convenience functionality to attach a Reaction and immediately fire the current state on the Reaction. - * - * @param f the function reaction - * @param priority the priority in comparison to other reactions (Defaults to Priority.Normal) - * @return Reaction[T] + * Gets the current value from the current `State` */ - def attachAndFire(f: T => Unit, priority: Double = Priority.Normal): Reaction[T] = { - val reaction = attach(f, priority) - fire(get, Some(get), List(reaction)) - reaction - } + override def get: T = Val.get(this) /** * Group multiple Vals together @@ -42,20 +49,64 @@ trait Val[T] extends Reactive[T] { /** * Group multiple Vals together */ - def and(that: Val[T]): Val[T] = ValGroup[T](None, List(this, that)) + def and(that: Val[T]): Val[T] = ValGroup[T](List(this, that)) - /** - * Functional mapping of this Val into another Val. - * - * @param f conversion function - * @tparam R the type of the new Val - * @return Val[R] - */ - def map[R](f: T => R): Val[R] = Val[R](f(get)) + def equality(t1: T, t2: T): Boolean = t1 == t2 - override def toString: String = name.getOrElse("Val") + override def toString: String = s"Var($evaluated)" } object Val { - def apply[T](value: => T, name: Option[String] = None): Val[T] = new StandardVal[T](value, name) + private val evaluating: ThreadLocal[Option[Evaluating]] = new ThreadLocal[Option[Evaluating]] { + override def initialValue(): Option[Evaluating] = None + } + + def apply[T](value: => T): Val[T] = { + val v = new Val[T] + v.set(value) + v + } + + def evaluate[T](v: Val[T], updating: Boolean): Unit = { + assert(evaluating.get().isEmpty, s"Expected empty evaluating, but found: ${evaluating.get()}") + val e = new Evaluating(v, updating) + evaluating.set(Some(e)) + val evaluated = try { + v.function() + } finally { + evaluating.set(None) + } + + val previousReferences = v._references + val removed = previousReferences.diff(e.references) + val added = e.references.diff(previousReferences) + removed.foreach(_.reactions.asInstanceOf[Reactions[Any]] -= v.reaction) + added.foreach(_.reactions.asInstanceOf[Reactions[Any]] += v.reaction) + if (evaluated != v.evaluated) { + v.previous = Option(v.evaluated) + v.evaluated = evaluated + + v.fire(evaluated, v.previous, v.reactions()) + } + } + + def get[T](v: Val[T]): T = { + evaluating.get() match { + case Some(e) if e.v eq v => { + if (e.updating) { + v.previous.getOrElse(throw new RuntimeException("Attempting to get previous on None!")) + } else { + v.evaluated + } + } + case Some(e) => { + e.references += v + + v.evaluated + } + case None => v.evaluated + } + } + + class Evaluating(val v: Val[_], val updating: Boolean, var references: Set[Val[_]] = Set.empty) } \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/Var.scala b/reactify/src/main/scala/reactify/Var.scala index 258875d..1e8d915 100644 --- a/reactify/src/main/scala/reactify/Var.scala +++ b/reactify/src/main/scala/reactify/Var.scala @@ -4,52 +4,26 @@ import java.util.concurrent.atomic.AtomicBoolean import reactify.bind.{BindSet, Binding} import reactify.group.VarGroup -import reactify.standard.StandardVar -/** - * Var represents the combination of `Val` and `Channel` into a stateful and mutable underlying value. - * - * @tparam T the type of value this Reactive receives - */ -trait Var[T] extends Val[T] with Channel[T] { - /** - * Operating mode of this Var. Defaults to `Normal` - */ - def mode: Var.Mode +class Var[T] protected() extends Val[T]() with Mutable[T] { + def this(f: => T) = { + this() - /** - * Statically sets a value without monitoring effects - * - * @param value the value to assign - */ - def static(value: T): Unit = set(value, Var.Mode.Static) + set(f) + } - override def @=(value: T): Unit = set(value, Var.Mode.Static) + override def set(value: => T): Unit = super.set(value) + override def static(f: T): Unit = super.static(f) /** * Group multiple Vars together */ def &(that: Var[T]): Var[T] = and(that) - def set(value: => T, mode: Var.Mode): Unit - /** * Group multiple Vars together */ - def and(that: Var[T]): Var[T] = VarGroup[T](None, List(this, that)) - - /** - * Functional mapping of this Var into another Var. - * - * @param f conversion function - * @tparam R the type of the new Var - * @return Var[R] - */ - override def map[R](f: T => R): Var[R] = { - val v = Var[R](f(get)) - attach(v := f(_)) - v - } + def and(that: Var[T]): Var[T] = VarGroup[T](List(this, that)) /** * Convenience method to create a binding between two `Var`s @@ -89,19 +63,12 @@ trait Var[T] extends Val[T] with Channel[T] { } new Binding(this, that, leftToRight, rightToLeft) } - - override def toString: String = name.getOrElse("Var") } object Var { - def apply[T](value: => T, - mode: Mode = Mode.Normal, - name: Option[String] = None): Var[T] = new StandardVar[T](value, mode, name) - - sealed trait Mode - - object Mode { - case object Normal extends Mode - case object Static extends Mode + def apply[T](f: => T): Var[T] = { + val v = new Var[T] + v.set(f) + v } } \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/group/ChannelGroup.scala b/reactify/src/main/scala/reactify/group/ChannelGroup.scala index d91fb81..c1ada26 100644 --- a/reactify/src/main/scala/reactify/group/ChannelGroup.scala +++ b/reactify/src/main/scala/reactify/group/ChannelGroup.scala @@ -1,13 +1,12 @@ package reactify.group import reactify.Channel -import reactify.reaction.{GroupReactions, Reactions} +import reactify.reaction.Reactions -case class ChannelGroup[T](override val name: Option[String], - items: List[Channel[T]]) extends Channel[T] with Group[T, Channel[T]] { +case class ChannelGroup[T](items: List[Channel[T]]) extends Channel[T] with Group[T, Channel[T]] { override lazy val reactions: Reactions[T] = new GroupReactions(this) override def set(value: => T): Unit = items.foreach(_.set(value)) - override def and(that: Channel[T]): Channel[T] = ChannelGroup(name, items ::: List(that)) + override def and(that: Channel[T]): Channel[T] = ChannelGroup(items ::: List(that)) } \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/reaction/GroupReactions.scala b/reactify/src/main/scala/reactify/group/GroupReactions.scala similarity index 79% rename from reactify/src/main/scala/reactify/reaction/GroupReactions.scala rename to reactify/src/main/scala/reactify/group/GroupReactions.scala index 202e41b..e666bf7 100644 --- a/reactify/src/main/scala/reactify/reaction/GroupReactions.scala +++ b/reactify/src/main/scala/reactify/group/GroupReactions.scala @@ -1,10 +1,9 @@ -package reactify.reaction +package reactify.group import reactify.Reactive -import reactify.group.Group -import reactify.standard.StandardReactions +import reactify.reaction.{Reaction, Reactions} -class GroupReactions[T, R <: Reactive[T]](group: Group[T, R]) extends StandardReactions[T] { +class GroupReactions[T, R <: Reactive[T]](group: Group[T, R]) extends Reactions[T] { override def +=(reaction: Reaction[T]): Reaction[T] = { group.items.foreach(_.reactions += reaction) super.+=(reaction) @@ -21,4 +20,4 @@ class GroupReactions[T, R <: Reactive[T]](group: Group[T, R]) extends StandardRe } super.clear() } -} \ No newline at end of file +} diff --git a/reactify/src/main/scala/reactify/group/StatefulGroup.scala b/reactify/src/main/scala/reactify/group/StatefulGroup.scala new file mode 100644 index 0000000..c2bc80e --- /dev/null +++ b/reactify/src/main/scala/reactify/group/StatefulGroup.scala @@ -0,0 +1,10 @@ +package reactify.group + +import reactify.Stateful +import reactify.reaction.Reactions + +case class StatefulGroup[T](items: List[Stateful[T]]) extends Stateful[T] with Group[T, Stateful[T]] { + override lazy val reactions: Reactions[T] = new GroupReactions(this) + + override def get: T = items.head.get +} \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/group/ValGroup.scala b/reactify/src/main/scala/reactify/group/ValGroup.scala index 6a7618d..6292de7 100644 --- a/reactify/src/main/scala/reactify/group/ValGroup.scala +++ b/reactify/src/main/scala/reactify/group/ValGroup.scala @@ -1,10 +1,12 @@ package reactify.group -import reactify.reaction.{GroupReactions, Reactions} import reactify.Val +import reactify.reaction.Reactions -case class ValGroup[T](override val name: Option[String], items: List[Val[T]]) extends Val[T] with Group[T, Val[T]] { - override lazy val reactions: Reactions[T] = new GroupReactions(this) +case class ValGroup[T](items: List[Val[T]]) extends Val[T] with Group[T, Val[T]] { + override lazy val reactions: Reactions[T] = new GroupReactions[T, Val[T]](this) + + override def and(that: Val[T]): Val[T] = ValGroup(items ::: List(that)) override def get: T = items.head.get -} \ No newline at end of file +} diff --git a/reactify/src/main/scala/reactify/group/VarGroup.scala b/reactify/src/main/scala/reactify/group/VarGroup.scala index 6e30117..ded1f13 100644 --- a/reactify/src/main/scala/reactify/group/VarGroup.scala +++ b/reactify/src/main/scala/reactify/group/VarGroup.scala @@ -1,18 +1,14 @@ package reactify.group -import reactify.reaction.{GroupReactions, Reactions} import reactify.Var +import reactify.reaction.Reactions -case class VarGroup[T](override val name: Option[String], items: List[Var[T]]) extends Var[T] with Group[T, Var[T]] { - override def mode: Var.Mode = items.head.mode - +case class VarGroup[T](items: List[Var[T]]) extends Var[T] with Group[T, Var[T]] { override lazy val reactions: Reactions[T] = new GroupReactions[T, Var[T]](this) override def set(value: => T): Unit = items.foreach(_.set(value)) - override def set(value: => T, mode: Var.Mode): Unit = items.foreach(_.set(value, mode)) - - override def and(that: Var[T]): Var[T] = VarGroup(name, items ::: List(that)) + override def and(that: Var[T]): Var[T] = VarGroup(items ::: List(that)) override def get: T = items.head.get } \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/prototype/Test.scala b/reactify/src/main/scala/reactify/prototype/Test.scala deleted file mode 100644 index 3d3dec9..0000000 --- a/reactify/src/main/scala/reactify/prototype/Test.scala +++ /dev/null @@ -1,92 +0,0 @@ -package reactify.prototype - -object Test { - def main(args: Array[String]): Unit = { - val v = Var(1) - println(s"Value: ${v.get}") - v.set(2) - println(s"Value: ${v.get}") - v.set(3 + v.get) - - val v2 = Var(v.get + 5) - v := 6 - } -} - -class Var[T] private() { - private var function: () => T = _ - private var evaluated: T = _ - private var previous: Option[T] = None - private var references: Set[Var[_]] = Set.empty - - def set(value: => T): Unit = { - function = () => value - Var.evaluate(this, updating = false) - } - - def static(value: T): Unit = { - function = () => value - previous = Option(evaluated) - evaluated = value - references = Set.empty - } - - def get: T = Var.get(this) - - def :=(value: => T): Unit = set(value) - def @=(value: T): Unit = static(value) - def apply(): T = get - - def equality(t1: T, t2: T): Boolean = t1 == t2 - - override def toString: String = s"Var($evaluated)" -} - -object Var { - private val evaluating: ThreadLocal[Option[Evaluating]] = new ThreadLocal[Option[Evaluating]] { - override def initialValue(): Option[Evaluating] = None - } - - def apply[T](value: => T): Var[T] = { - val v = new Var[T] - v.set(value) - v - } - - def evaluate[T](v: Var[T], updating: Boolean): Unit = { - assert(evaluating.get().isEmpty, s"Expected empty evaluating, but found: ${evaluating.get()}") - val e = new Evaluating(v, updating) - evaluating.set(Some(e)) - val evaluated = try { - v.function() - } finally { - evaluating.set(None) - } - if (evaluated != v.evaluated) { - v.previous = Option(v.evaluated) - v.evaluated = evaluated - // TODO: Fire - println(s"Modified! Previous: ${v.previous}, Current: $evaluated, References: ${e.references}") - } - } - - def get[T](v: Var[T]): T = { - evaluating.get() match { - case Some(e) if e.v eq v => { - if (e.updating) { - v.previous.getOrElse(throw new RuntimeException("Attempting to get previous on None!")) - } else { - v.evaluated - } - } - case Some(e) => { - e.references += v - - v.evaluated - } - case None => v.evaluated - } - } -} - -class Evaluating(val v: Var[_], val updating: Boolean, var references: Set[Var[_]] = Set.empty) \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/reaction/ChangeFunctionReaction.scala b/reactify/src/main/scala/reactify/reaction/ChangeFunctionReaction.scala index 0fee5de..8c3947f 100644 --- a/reactify/src/main/scala/reactify/reaction/ChangeFunctionReaction.scala +++ b/reactify/src/main/scala/reactify/reaction/ChangeFunctionReaction.scala @@ -9,6 +9,6 @@ case class ChangeFunctionReaction[T](f: (T, T) => Unit, override val priority: D } ReactionStatus.Continue } finally { - this.previous = Some(value) + this.previous = Option(value) } } diff --git a/reactify/src/main/scala/reactify/reaction/FunctionReaction.scala b/reactify/src/main/scala/reactify/reaction/FunctionReaction.scala index 8677bff..d5d32ae 100644 --- a/reactify/src/main/scala/reactify/reaction/FunctionReaction.scala +++ b/reactify/src/main/scala/reactify/reaction/FunctionReaction.scala @@ -5,4 +5,4 @@ case class FunctionReaction[T](f: T => Unit, override val priority: Double) exte f(value) ReactionStatus.Continue } -} \ No newline at end of file +} diff --git a/reactify/src/main/scala/reactify/reaction/Reaction.scala b/reactify/src/main/scala/reactify/reaction/Reaction.scala index 80feb36..0a2e79c 100644 --- a/reactify/src/main/scala/reactify/reaction/Reaction.scala +++ b/reactify/src/main/scala/reactify/reaction/Reaction.scala @@ -3,8 +3,8 @@ package reactify.reaction import reactify.Priority /** - * Reaction may be thought of similar to `Observer` or `Listener` in other libraries. A Reaction may be added to a - * Reactive in order react when a value is fired upon it. + * Reaction may be considered similar to `Observer` or `Listener` in other libraries. A Reaction may be added to a + * Reactive in order to react when a value is fired upon it. * * @tparam T the type received by this Reaction */ diff --git a/reactify/src/main/scala/reactify/reaction/Reactions.scala b/reactify/src/main/scala/reactify/reaction/Reactions.scala index bff1f54..5f6afc2 100644 --- a/reactify/src/main/scala/reactify/reaction/Reactions.scala +++ b/reactify/src/main/scala/reactify/reaction/Reactions.scala @@ -3,24 +3,35 @@ package reactify.reaction /** * Reactions represents a list of Reaction instances specifically associated with a Reactive */ -trait Reactions[T] { +class Reactions[T] { + private var list = List.empty[Reaction[T]] + /** * Return all Reactions associated with this Reactive */ - def apply(): List[Reaction[T]] + def apply(): List[Reaction[T]] = list /** * Add a Reaction */ - def +=(reaction: Reaction[T]): Reaction[T] + def +=(reaction: Reaction[T]): Reaction[T] = synchronized { + list = (list ::: List(reaction)).sorted.distinct + reaction + } /** * Remove a Reaction */ - def -=(reaction: Reaction[T]): Boolean + def -=(reaction: Reaction[T]): Boolean = synchronized { + val previous = list + list = list.filterNot(_ eq reaction) + previous != list + } /** * Remove all Reactions */ - def clear(): Unit -} \ No newline at end of file + def clear(): Unit = synchronized { + list = Nil + } +} diff --git a/reactify/src/main/scala/reactify/standard/StandardChannel.scala b/reactify/src/main/scala/reactify/standard/StandardChannel.scala deleted file mode 100644 index df6ef32..0000000 --- a/reactify/src/main/scala/reactify/standard/StandardChannel.scala +++ /dev/null @@ -1,7 +0,0 @@ -package reactify.standard - -import reactify.Channel - -class StandardChannel[T](override val name: Option[String]) extends Channel[T] { - override def set(value: => T): Unit = fire(value, None, reactions()) -} diff --git a/reactify/src/main/scala/reactify/standard/StandardDep.scala b/reactify/src/main/scala/reactify/standard/StandardDep.scala deleted file mode 100644 index 0cdde5e..0000000 --- a/reactify/src/main/scala/reactify/standard/StandardDep.scala +++ /dev/null @@ -1,18 +0,0 @@ -package reactify.standard - -import reactify.{Dep, Var} - -class StandardDep[T, R](override val name: Option[String], - override val owner: Var[R], - r2TFunction: R => T, - t2RFunction: T => R) extends StandardVal[T](r2TFunction(owner), name) with Dep[T, R] { - override def mode: Var.Mode = Var.Mode.Normal - - override def set(value: => T): Unit = owner := t2R(value) - - override def set(value: => T, mode: Var.Mode): Unit = owner.set(t2R(value), mode) - - override def t2R(t: T): R = t2RFunction(t) - - override def r2T(r: R): T = r2TFunction(r) -} \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/standard/StandardReactions.scala b/reactify/src/main/scala/reactify/standard/StandardReactions.scala deleted file mode 100644 index c982211..0000000 --- a/reactify/src/main/scala/reactify/standard/StandardReactions.scala +++ /dev/null @@ -1,24 +0,0 @@ -package reactify.standard - -import reactify.reaction.{Reaction, Reactions} - -class StandardReactions[T] extends Reactions[T] { - var list = List.empty[Reaction[T]] - - def apply(): List[Reaction[T]] = list - - def +=(reaction: Reaction[T]): Reaction[T] = synchronized { - list = (list ::: List(reaction)).sorted.distinct - reaction - } - - def -=(reaction: Reaction[T]): Boolean = synchronized { - val previous = list - list = list.filterNot(_ eq reaction) - previous != list - } - - def clear(): Unit = synchronized { - list = Nil - } -} diff --git a/reactify/src/main/scala/reactify/standard/StandardVal.scala b/reactify/src/main/scala/reactify/standard/StandardVal.scala deleted file mode 100644 index c435156..0000000 --- a/reactify/src/main/scala/reactify/standard/StandardVal.scala +++ /dev/null @@ -1,48 +0,0 @@ -package reactify.standard - -import reactify.reaction.{Reaction, ReactionStatus, Reactions} -import reactify.{FunctionResult, Reactive, Val, Var} - -class StandardVal[T](f: => T, override val name: Option[String]) extends Val[T] { - private var result: FunctionResult[T] = _ - refresh(f, mode, replacing = true) - - protected def mode: Var.Mode = Var.Mode.Normal - - private lazy val reaction: Reaction[Any] = new Reaction[Any] { - override def apply(value: Any, previous: Option[Any]): ReactionStatus = { - println(s"reaction recalculate: $value, previous: $previous") - recalculate() - ReactionStatus.Continue - } - } - - protected def recalculate(): Unit = { - println("Recalculating!") - refresh(result.function(), result.mode, replacing = false) - } - - protected def refresh(f: => T, mode: Var.Mode, replacing: Boolean): Unit = synchronized { - val previousValue = Option(result).map(_.value) - val previousReferences = Option(result).map(_.referenced).getOrElse(Set.empty) - val previous = if (replacing) { - previousValue.getOrElse(null.asInstanceOf[T]) - } else { - Option(result).map(_.previous).getOrElse(null.asInstanceOf[T]) - } - mode match { - case Var.Mode.Normal => result = Reactive.processing(this, f, mode, previous) - case Var.Mode.Static => result = new FunctionResult[T](() => f, mode, f, previous, Set.empty) - } - val removed = previousReferences.diff(result.referenced) - val added = result.referenced.diff(previousReferences) - removed.foreach(_.reactions.asInstanceOf[Reactions[Any]] -= reaction) - added.foreach(_.reactions.asInstanceOf[Reactions[Any]] += reaction) - - if (!previousValue.contains(result.value)) { - fire(result.value, previousValue) - } - } - - override def get: T = Reactive.getting(this).getOrElse(result.value) -} \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/standard/StandardVar.scala b/reactify/src/main/scala/reactify/standard/StandardVar.scala deleted file mode 100644 index 11b4202..0000000 --- a/reactify/src/main/scala/reactify/standard/StandardVar.scala +++ /dev/null @@ -1,9 +0,0 @@ -package reactify.standard - -import reactify.Var - -class StandardVar[T](f: => T, override val mode: Var.Mode, name: Option[String]) extends StandardVal[T](f, name) with Var[T] { - override def set(value: => T): Unit = set(value, mode) - - override def set(value: => T, mode: Var.Mode): Unit = refresh(value, mode, replacing = true) -} \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/transaction/Transaction.scala b/reactify/src/main/scala/reactify/transaction/Transaction.scala deleted file mode 100644 index 93c407e..0000000 --- a/reactify/src/main/scala/reactify/transaction/Transaction.scala +++ /dev/null @@ -1,156 +0,0 @@ -package reactify.transaction - -import reactify.Var - -class Transaction { - private var map = Map.empty[Var[_], TransactionChange] - - /** - * Called when the value of a Var changes - */ - def change[T](owner: Var[T], oldFunction: () => T, newFunction: () => T): Unit = { - val change = map.get(owner) match { - case Some(c) => c.copy(apply = () => owner := newFunction()) - case None => TransactionChange(() => owner := oldFunction(), () => owner := newFunction()) - } - map += owner -> change - } - - /** - * Gets the `TransactionChange` for the supplied `Var` if one is defined - */ - def get[T](v: Var[T]): Option[TransactionChange] = map.get(v) - - /** - * Returns the `TransactionChange` for the supplied `Var` or throws an exception if none exists - */ - def apply[T](v: Var[T]): TransactionChange = get(v).getOrElse(throw new RuntimeException(s"No reference in transaction for $v")) - - /** - * Commits all changes in this Transaction and then clears the transaction - */ - def commit(): Unit = { - map.keys.foreach { v => - commit(v.asInstanceOf[Var[Any]]) - } - map = Map.empty - } - - /** - * Reverts all changes in this Transaction and then clears the transaction - */ - def revert(): Unit = { - map.keys.foreach { v => - revert(v.asInstanceOf[Var[Any]]) - } - map = Map.empty - } - - /** - * Undoes all changes that occurred within this Transaction. Unlike `revert`, this doesn't clear the transaction. - * This allows `redo` to run to re-apply the transaction in the future. - */ - def undo(): Unit = { - map.keys.foreach { v => - undo(v.asInstanceOf[Var[Any]]) - } - } - - /** - * Redoes all changes that occurred within this Transaction. Unlike `commit`, this doesn't clear the transaction. - * This allows `undo` to un-apply the transaction in the future. - */ - def redo(): Unit = { - map.keys.foreach { v => - redo(v.asInstanceOf[Var[Any]]) - } - } - - /** - * Redoes the transaction for this `Var` and then clears it from the transaction. - * - * @return true if a change was applied - */ - def commit[T](v: Var[T]): Boolean = if (redo(v)) { - map -= v - true - } else { - false - } - - /** - * Undoes the transaction for this `Var` and then clears it from the transaction. - * - * @return true if a change was applied - */ - def revert[T](v: Var[T]): Boolean = if (undo(v)) { - map -= v - true - } else { - false - } - - /** - * Undoes the transaction for this `Var`. - */ - def undo[T](v: Var[T]): Boolean = get(v) match { - case Some(change) => { - change.unapply() - true - } - case None => false - } - - /** - * Redoes the transaction for this `Var`. - */ - def redo[T](v: Var[T]): Boolean = get(v) match { - case Some(change) => { - change.apply() - true - } - case None => false - } -} - -/** - * Transaction allows access to undo, redo, revert, and commit changes to `Var`s - */ -object Transaction { - private val threadLocal = new ThreadLocal[Option[Transaction]] { - override def initialValue(): Option[Transaction] = None - } - - /** - * True if a Transaction is currently active on the current thread - */ - def active: Boolean = threadLocal.get().nonEmpty - - /** - * Creates a new Transaction if one isn't already active or re-uses an existing one if a Transaction is already - * in-progress for this thread. - * - * @param f the function to run within a Transaction - * @return Transaction - */ - def apply(f: => Unit): Transaction = { - val created = !active - val transaction = threadLocal.get().getOrElse { - val t = new Transaction - threadLocal.set(Some(t)) - t - } - f - if (created) { - threadLocal.remove() - } - transaction - } - - /** - * Called when the value of a Var changes - */ - def change[T](owner: Var[T], oldFunction: () => T, newFunction: () => T): Unit = { - threadLocal.get().foreach(_.change(owner, oldFunction, newFunction)) - } -} \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/transaction/TransactionChange.scala b/reactify/src/main/scala/reactify/transaction/TransactionChange.scala deleted file mode 100644 index db252be..0000000 --- a/reactify/src/main/scala/reactify/transaction/TransactionChange.scala +++ /dev/null @@ -1,9 +0,0 @@ -package reactify.transaction - -/** - * TransactionChange represents the transactional changes for a single `Var` in a `Transaction` - * - * @param unapply reverts the changes applied during the transaction - * @param apply applies the changes applied during the transaction - */ -case class TransactionChange(unapply: () => Unit, apply: () => Unit) \ No newline at end of file diff --git a/reactify/src/test/scala/test/BindingSpec.scala b/reactify/src/test/scala/test/BindingSpec.scala index 4370306..b522feb 100644 --- a/reactify/src/test/scala/test/BindingSpec.scala +++ b/reactify/src/test/scala/test/BindingSpec.scala @@ -125,4 +125,4 @@ class BindingSpec extends AnyWordSpec with Matchers { v1() should be("Goodbye") } } -} \ No newline at end of file +} diff --git a/reactify/src/test/scala/test/DepSpec.scala b/reactify/src/test/scala/test/DepSpec.scala index f8c21eb..798519c 100644 --- a/reactify/src/test/scala/test/DepSpec.scala +++ b/reactify/src/test/scala/test/DepSpec.scala @@ -101,8 +101,8 @@ class DepSpec extends AnyWordSpec with Matchers { changes() should be(List( List(75.0 -> 50.0), - List(87.5 -> 75.0), - Nil + List(87.5 -> 100.0, 100.0 -> 75.0), + List(100.0 -> 125.0, 125.0 -> 100.0) )) } "set center and reflect properly in left and right" in { @@ -131,8 +131,8 @@ class DepSpec extends AnyWordSpec with Matchers { changes() should be(List( List(175.0 -> 150.0), - Nil, - List(225.0 -> 250.0) + List(200.0 -> 225.0, 225.0 -> 200.0), + List(225.0 -> 275.0, 275.0 -> 250.0) )) } "set left and verify center and right adjust" in { @@ -166,4 +166,4 @@ class DepSpec extends AnyWordSpec with Matchers { )) } } -} \ No newline at end of file +} diff --git a/reactify/src/test/scala/test/DepSpecialSpec.scala b/reactify/src/test/scala/test/DepSpecialSpec.scala index 2d3aa81..bbfc949 100644 --- a/reactify/src/test/scala/test/DepSpecialSpec.scala +++ b/reactify/src/test/scala/test/DepSpecialSpec.scala @@ -33,10 +33,10 @@ class DepSpecialSpec extends AnyWordSpec with Matchers { } } "validating derived observables" should { - val left = Var(0.0, name = Some("left")) - val width = Var(0.0, name = Some("width")) - val center = Dep[Double, Double](left, "center")(_ + width / 2.0, _ - width / 2.0) - val right = Dep[Double, Double](left, "right")(_ + width, _ - width) + val left = Var(0.0) + val width = Var(0.0) + val center = Dep[Double, Double](left)(_ + width / 2.0, _ - width / 2.0) + val right = Dep[Double, Double](left)(_ + width, _ - width) var leftValue = left() left.attach(d => leftValue = d) @@ -60,22 +60,22 @@ class DepSpecialSpec extends AnyWordSpec with Matchers { // } } "propagation of values" should { - val left = Var(0.0, name = Some("left")) - val width = Var(0.0, name = Some("width")) - val center = Dep[Double, Double](left, "center")(_ + width / 2.0, _ - width / 2.0) - val right = Dep[Double, Double](left, "right")(_ + width, _ - width) + val left = Var(0.0) + val width = Var(0.0) + val center = Dep[Double, Double](left)(_ + width / 2.0, _ - width / 2.0) + val right = Dep[Double, Double](left)(_ + width, _ - width) - val arbitrary = Var(0.0, name = Some("arbitrary")) + val arbitrary = Var(0.0) "set the right to an arbitrary value" in { right := arbitrary arbitrary := 100.0 + List(left(), center(), right()) should be(List(100.0, 100.0, 100.0)) + width := 50.0 - right() should be(100.0) - left() should be(50.0) - center() should be(75.0) + List(left(), center(), right()) should be(List(50.0, 75.0, 100.0)) } } } diff --git a/reactify/src/test/scala/test/TransactionSpec.scala b/reactify/src/test/scala/test/TransactionSpec.scala index 6f44e58..0db6ecd 100644 --- a/reactify/src/test/scala/test/TransactionSpec.scala +++ b/reactify/src/test/scala/test/TransactionSpec.scala @@ -1,3 +1,4 @@ +/* package test import org.scalatest.matchers.should.Matchers @@ -29,3 +30,4 @@ class TransactionSpec extends AnyWordSpec with Matchers { } } } +*/ diff --git a/reactify/src/test/scala/test/VarSpec.scala b/reactify/src/test/scala/test/VarSpec.scala index 42fc315..7981847 100644 --- a/reactify/src/test/scala/test/VarSpec.scala +++ b/reactify/src/test/scala/test/VarSpec.scala @@ -183,9 +183,9 @@ class VarSpec extends AnyWordSpec with Matchers { v() should be(3) } "create a list that is dependent on vars" in { - val s1 = Var("One", name = Some("s1")) - val s2 = Var("Two", name = Some("s2")) - val list = Var(List.empty[String], name = Some("list")) + val s1 = Var("One") + val s2 = Var("Two") + val list = Var(List.empty[String]) list := s1() :: s2() :: Nil list() should be(List("One", "Two")) // list.state.index should be(2) From 85a2be80cfd8b51f2c6f146501367a6b1517f3a2 Mon Sep 17 00:00:00 2001 From: Matt Hicks Date: Thu, 19 Mar 2020 14:53:36 -0500 Subject: [PATCH 5/6] Clean up, addition of transaction, and updates to README --- README.md | 5 +- .../src/main/scala/reactify/Channel.scala | 5 + .../src/main/scala/reactify/Stateful.scala | 6 +- .../src/main/scala/reactify/Trigger.scala | 3 + reactify/src/main/scala/reactify/Val.scala | 4 +- reactify/src/main/scala/reactify/Var.scala | 24 ++- .../src/main/scala/reactify/package.scala | 2 +- .../reactify/transaction/Transaction.scala | 168 ++++++++++++++++++ .../src/test/scala/test/TransactionSpec.scala | 2 - reactify/src/test/scala/test/VarSpec.scala | 59 +----- 10 files changed, 211 insertions(+), 67 deletions(-) create mode 100644 reactify/src/main/scala/reactify/transaction/Transaction.scala diff --git a/README.md b/README.md index d9635b5..8150761 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Build Status](https://travis-ci.org/outr/reactify.svg?branch=master)](https://travis-ci.org/outr/reactify) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/759324d19db5496dbd9867b4a113c806)](https://www.codacy.com/app/matthicks/reactify?utm_source=github.com&utm_medium=referral&utm_content=outr/reactify&utm_campaign=Badge_Grade) [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/759324d19db5496dbd9867b4a113c806)](https://www.codacy.com/app/matthicks/reactify?utm_source=github.com&utm_medium=referral&utm_content=outr/reactify&utm_campaign=Badge_Coverage) -[![Stories in Ready](https://badge.waffle.io/outr/reactify.png?label=ready&title=Ready)](https://waffle.io/outr/reactify) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/outr/reactify) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.outr/reactify_2.12/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.outr/reactify_2.12) [![Latest version](https://index.scala-lang.org/outr/reactify/reactify/latest.svg)](https://index.scala-lang.org/outr/reactify) @@ -34,13 +33,13 @@ reactify is published to Sonatype OSS and Maven Central currently supporting: Configuring the dependency in SBT simply requires: ``` -libraryDependencies += "com.outr" %% "reactify" % "3.0.3" +libraryDependencies += "com.outr" %% "reactify" % "4.0.0" ``` or, for Scala.js / Scala Native / cross-building: ``` -libraryDependencies += "com.outr" %%% "reactify" % "3.0.3" +libraryDependencies += "com.outr" %%% "reactify" % "4.0.0" ``` ## Concepts diff --git a/reactify/src/main/scala/reactify/Channel.scala b/reactify/src/main/scala/reactify/Channel.scala index 0fc73b2..ec7b35f 100644 --- a/reactify/src/main/scala/reactify/Channel.scala +++ b/reactify/src/main/scala/reactify/Channel.scala @@ -2,6 +2,11 @@ package reactify import reactify.group.ChannelGroup +/** + * Channel is a stateless Reactive implementation exposing a public method to fire values. + * + * @tparam T the type of value this Reactive receives + */ class Channel[T] extends Reactive[T] with Mutable[T] { override def set(f: => T): Unit = fire(f, None, reactions()) diff --git a/reactify/src/main/scala/reactify/Stateful.scala b/reactify/src/main/scala/reactify/Stateful.scala index 06c2cf9..ef93b24 100644 --- a/reactify/src/main/scala/reactify/Stateful.scala +++ b/reactify/src/main/scala/reactify/Stateful.scala @@ -5,7 +5,7 @@ import reactify.reaction.Reaction trait Stateful[T] extends Reactive[T] { /** - * Gets the current value from the current `State` + * Gets the current value */ def get: T @@ -28,12 +28,12 @@ trait Stateful[T] extends Reactive[T] { } /** - * Group multiple Vals together + * Group multiple Statefuls together */ def &(that: Stateful[T]): Stateful[T] = and(that) /** - * Group multiple Vals together + * Group multiple Statefuls together */ def and(that: Stateful[T]): Stateful[T] = StatefulGroup[T](List(this, that)) } \ No newline at end of file diff --git a/reactify/src/main/scala/reactify/Trigger.scala b/reactify/src/main/scala/reactify/Trigger.scala index ec9f87b..017961a 100644 --- a/reactify/src/main/scala/reactify/Trigger.scala +++ b/reactify/src/main/scala/reactify/Trigger.scala @@ -5,6 +5,9 @@ package reactify * just the reactions themselves. */ class Trigger extends Channel[Unit] { + /** + * Fires an event on this trigger + */ def trigger(): Unit = fire((), None) } diff --git a/reactify/src/main/scala/reactify/Val.scala b/reactify/src/main/scala/reactify/Val.scala index ff001a0..fed73c6 100644 --- a/reactify/src/main/scala/reactify/Val.scala +++ b/reactify/src/main/scala/reactify/Val.scala @@ -68,13 +68,13 @@ object Val { } def evaluate[T](v: Val[T], updating: Boolean): Unit = { - assert(evaluating.get().isEmpty, s"Expected empty evaluating, but found: ${evaluating.get()}") + val original = evaluating.get() val e = new Evaluating(v, updating) evaluating.set(Some(e)) val evaluated = try { v.function() } finally { - evaluating.set(None) + evaluating.set(original) } val previousReferences = v._references diff --git a/reactify/src/main/scala/reactify/Var.scala b/reactify/src/main/scala/reactify/Var.scala index 1e8d915..7753bca 100644 --- a/reactify/src/main/scala/reactify/Var.scala +++ b/reactify/src/main/scala/reactify/Var.scala @@ -4,7 +4,13 @@ import java.util.concurrent.atomic.AtomicBoolean import reactify.bind.{BindSet, Binding} import reactify.group.VarGroup +import reactify.transaction.Transaction +/** + * Var represents the combination of `Val` and `Channel` into a stateful and mutable underlying value. + * + * @tparam T the type of value this Reactive receives + */ class Var[T] protected() extends Val[T]() with Mutable[T] { def this(f: => T) = { this() @@ -12,8 +18,22 @@ class Var[T] protected() extends Val[T]() with Mutable[T] { set(f) } - override def set(value: => T): Unit = super.set(value) - override def static(f: T): Unit = super.static(f) + /** + * Sets a new functional value to this var + * + * @param value the functional value to assign + */ + override def set(value: => T): Unit = { + Transaction.change(this, this.function, () => value) + super.set(value) + } + + /** + * Statically sets a value without monitoring effects + * + * @param value the value to assign + */ + override def static(value: T): Unit = super.static(value) /** * Group multiple Vars together diff --git a/reactify/src/main/scala/reactify/package.scala b/reactify/src/main/scala/reactify/package.scala index bd6d7e3..589108f 100644 --- a/reactify/src/main/scala/reactify/package.scala +++ b/reactify/src/main/scala/reactify/package.scala @@ -1,7 +1,7 @@ import scala.language.implicitConversions package object reactify { - implicit def val2Value[T](v: Val[T]): T = v() + implicit def stateful2Value[T](v: Stateful[T]): T = v() /** * Syntactic sugar for mutating collections in a `Var` diff --git a/reactify/src/main/scala/reactify/transaction/Transaction.scala b/reactify/src/main/scala/reactify/transaction/Transaction.scala new file mode 100644 index 0000000..a8538e2 --- /dev/null +++ b/reactify/src/main/scala/reactify/transaction/Transaction.scala @@ -0,0 +1,168 @@ +package reactify.transaction + +import reactify.Var + +class Transaction { + private var map = Map.empty[Var[_], TransactionChange] + + /** + * Called when the value of a Var changes + */ + def change[T](owner: Var[T], oldFunction: () => T, newFunction: () => T): Unit = { + val change = map.get(owner) match { + case Some(c) => c.copy(apply = () => owner := newFunction()) + case None => TransactionChange(() => owner := oldFunction(), () => owner := newFunction()) + } + map += owner -> change + } + + /** + * Gets the `TransactionChange` for the supplied `Var` if one is defined + */ + def get[T](v: Var[T]): Option[TransactionChange] = map.get(v) + + /** + * Returns the `TransactionChange` for the supplied `Var` or throws an exception if none exists + */ + def apply[T](v: Var[T]): TransactionChange = get(v).getOrElse(throw new RuntimeException(s"No reference in transaction for $v")) + + /** + * Commits all changes in this Transaction and then clears the transaction + */ + def commit(): Unit = { + map.keys.foreach { v => + commit(v.asInstanceOf[Var[Any]]) + } + map = Map.empty + } + + /** + * Reverts all changes in this Transaction and then clears the transaction + */ + def revert(): Unit = { + map.keys.foreach { v => + revert(v.asInstanceOf[Var[Any]]) + } + map = Map.empty + } + + /** + * Undoes all changes that occurred within this Transaction. Unlike `revert`, this doesn't clear the transaction. + * This allows `redo` to run to re-apply the transaction in the future. + */ + def undo(): Unit = { + map.keys.foreach { v => + undo(v.asInstanceOf[Var[Any]]) + } + } + + /** + * Redoes all changes that occurred within this Transaction. Unlike `commit`, this doesn't clear the transaction. + * This allows `undo` to un-apply the transaction in the future. + */ + def redo(): Unit = { + map.keys.foreach { v => + redo(v.asInstanceOf[Var[Any]]) + } + } + + /** + * Redoes the transaction for this `Var` and then clears it from the transaction. + * + * @return true if a change was applied + */ + def commit[T](v: Var[T]): Boolean = if (redo(v)) { + map -= v + true + } else { + false + } + + /** + * Undoes the transaction for this `Var` and then clears it from the transaction. + * + * @return true if a change was applied + */ + def revert[T](v: Var[T]): Boolean = if (undo(v)) { + map -= v + true + } else { + false + } + + /** + * Undoes the transaction for this `Var`. + */ + def undo[T](v: Var[T]): Boolean = get(v) match { + case Some(change) => { + change.unapply() + true + } + case None => false + } + + /** + * Redoes the transaction for this `Var`. + */ + def redo[T](v: Var[T]): Boolean = get(v) match { + case Some(change) => { + change.apply() + true + } + case None => false + } +} + +/** + * Transaction allows access to undo, redo, revert, and commit changes to `Var`s + */ +object Transaction { + private val threadLocal = new ThreadLocal[Option[Transaction]] { + override def initialValue(): Option[Transaction] = None + } + + /** + * True if a Transaction is currently active on the current thread + */ + def active: Boolean = threadLocal.get().nonEmpty + + /** + * Creates a new Transaction if one isn't already active or re-uses an existing one if a Transaction is already + * in-progress for this thread. + * + * @param f the function to run within a Transaction + * @return Transaction + */ + def apply(f: => Unit): Transaction = { + val created = !active + val transaction = threadLocal.get().getOrElse { + val t = new Transaction + threadLocal.set(Some(t)) + t + } + f + if (created) { + threadLocal.remove() + } + transaction + } + + /** + * Called when the value of a Var changes + */ + def change[T](owner: Var[T], oldFunction: () => T, newFunction: () => T): Boolean = threadLocal.get() match { + case Some(t) => { + t.change(owner, oldFunction, newFunction) + true + } + case None => false + } +} + +/** + * TransactionChange represents the transactional changes for a single `Var` in a `Transaction` + * + * @param unapply reverts the changes applied during the transaction + * @param apply applies the changes applied during the transaction + */ +case class TransactionChange(unapply: () => Unit, apply: () => Unit) \ No newline at end of file diff --git a/reactify/src/test/scala/test/TransactionSpec.scala b/reactify/src/test/scala/test/TransactionSpec.scala index 0db6ecd..6f44e58 100644 --- a/reactify/src/test/scala/test/TransactionSpec.scala +++ b/reactify/src/test/scala/test/TransactionSpec.scala @@ -1,4 +1,3 @@ -/* package test import org.scalatest.matchers.should.Matchers @@ -30,4 +29,3 @@ class TransactionSpec extends AnyWordSpec with Matchers { } } } -*/ diff --git a/reactify/src/test/scala/test/VarSpec.scala b/reactify/src/test/scala/test/VarSpec.scala index 7981847..49dd22c 100644 --- a/reactify/src/test/scala/test/VarSpec.scala +++ b/reactify/src/test/scala/test/VarSpec.scala @@ -32,8 +32,6 @@ class VarSpec extends AnyWordSpec with Matchers { val v2 = Var(s"Hello, ${v1()}") v1.reactions().size should be(1) v2.reactions().size should be(0) -// v2.state.references.size should be(1) -// v2.state.references should be(List(v1.state)) v2.on(changed += 1) v2.reactions().size should be(1) v2() should be("Hello, Matt") @@ -136,47 +134,6 @@ class VarSpec extends AnyWordSpec with Matchers { v := v * 4 v() should be(12) } - /*"derive a value from itself depending on another value" in { - val v1 = Var(1) - val v2 = Var(v1 + 1) - - v1.reactions() should be(List(v2.state)) - v2.reactions() should be(Nil) - - val v1State1 = v1.state - val v2State1 = v2.state - - v1State1.value should be(1) - v1State1.previousState should be(None) - v2State1.value should be(2) - v2State1.previousState should be(None) - - v2() should be(2) - v2 := v2 * 2 - - val v2State2 = v2.state - v2State1 should not be v2State2 - v1.state.value should be(1) - v2State2.previousState should be(Some(v2State1)) - v2State1.previousState should be(None) - v2State1.nextState should be(Some(v2.state)) - - v2() should be(4) - v1 := 2 - - val v1State2 = v1.state - - // Disconnected because no recurrent reference found - v1State2.previousState should be(None) - v1State1.previousState should be(None) - - v2State2.previousState should be(Some(v2State1)) - v2State1.previousState should be(None) - v2State1.nextState should be(Some(v2State2)) - v2State2.nextState should be(None) - - v2() should be(6) - }*/ "create a variable that builds upon itself multiple times" in { val v = Var(1) v := v + v + v @@ -188,20 +145,14 @@ class VarSpec extends AnyWordSpec with Matchers { val list = Var(List.empty[String]) list := s1() :: s2() :: Nil list() should be(List("One", "Two")) -// list.state.index should be(2) -// list.state.references.toSet should be(Set(s1.state, s2.state, list.state.previousState.get)) -// s2.reactions() should contain(list.state) s2 := "Three" -// list.state.index should be(2) list() should be(List("One", "Three")) s1 := "Two" list() should be(List("Two", "Three")) -// list := "One" :: list() -// list() should be(List("One", "Two", "Three")) -// s2 := "Four" -// list() should be(List("One", "Two", "Four")) + list := "One" :: list() + list() should be(List("One", "Two", "Three")) } - /*"create a Container with a generic Child list" in { + "create a Container with a generic Child list" in { val v1 = Var("One") val v2 = Var("Two") val container = new Container[String] @@ -361,7 +312,7 @@ class VarSpec extends AnyWordSpec with Matchers { val modified = ListBuffer.empty[Int] - VarGroup(None, List(v1, v2, v3)).attach { i => + VarGroup(List(v1, v2, v3)).attach { i => modified += i } @@ -412,7 +363,7 @@ class VarSpec extends AnyWordSpec with Matchers { val v = Var[Double](lazyDouble) lazyDouble := 100.0 v() should be(100.0) - }*/ + } } class Container[Child] { From 09f4d6eae8b8a1adbbcb2a797f348fa2d919a90f Mon Sep 17 00:00:00 2001 From: Matt Hicks Date: Thu, 19 Mar 2020 15:02:10 -0500 Subject: [PATCH 6/6] Fixes to travis file --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0c33977..0db3cd5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,13 +2,13 @@ language: scala sudo: required dist: trusty scala: - - 2.12.8 + - 2.13.1 jdk: - oraclejdk8 before_script: - curl https://raw.githubusercontent.com/scala-native/scala-native/master/scripts/travis_setup.sh | bash -x script: - - sbt +clean +test + - sbt +clean +reactifyJVM/test +reactifyJS/test ++2.11.12 reactifyNative/test - sbt coverage reactifyJVM/test - sbt coverageReport - sbt coverageAggregate