You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Signal(...) syntax can also be used to define a signal that has always the same value:
675
+
#+begin_src scala
676
+
val sig = Signal(3) // the signal that is always 3.
677
+
#+end_src
678
+
*** Time-Varying Signals
679
+
How do we define a signal that varies in time?
680
+
- We can use externally defined signals, such as =mousePosition= and =map= over them.
681
+
- Or we can use a Var.
682
+
*** Variable Signals
683
+
Value of type =Signal= are immutable.
684
+
685
+
But our library also defines a subclass =Var= of =Signal= for signals that can be changed.
686
+
687
+
=Var= provides an "update" operation, which allows to redefine the value of a signal from the current time on.
688
+
#+begin_src scala
689
+
val sig = Var(3)
690
+
sig.update(5) // the same as sig() = 5, since in scala f(E_1,...,E_n) = E == f.update(E_1,...,E_n,E)
691
+
#+end_src
692
+
*** Signals and Variables
693
+
Signals of type =Var= look a bit like mutable variables, where =sig()= is dereferencing, and =sig() = newValue= is update.
694
+
695
+
But there's a crucial difference:
696
+
#+begin_src scala
697
+
/* mutable var signal var */
698
+
a = 2 a() = 2
699
+
l = 2*a l() = 2*a()
700
+
a = a + 1 a()
701
+
l = 2 * a
702
+
#+end_src
703
+
** A Simple FRP Implementation
704
+
*** Summary: The Signal API
705
+
#+begin_src scala
706
+
class Signal[T](expr: => T) {
707
+
def apply(): T = ???
708
+
}
709
+
710
+
object Signal {
711
+
def applay[T](expr: => T) = new Signal(expr)
712
+
}
713
+
#+end_src
714
+
*** Summary: The Var API
715
+
#+begin_src scala
716
+
class Var[T](expr: => T) extends Signal[T](expr) {
717
+
override def update(expr: => T): Unit = super.update(expr)
718
+
}
719
+
720
+
object Var {
721
+
def apply[T](expr: => T) = new Var(expr)
722
+
}
723
+
#+end_src
724
+
*** Implementation Idea
725
+
Each signal maintains
726
+
- its current value
727
+
- the current expression that defines the signal value
728
+
- a set of /observers/: the other signals that depend on its value
729
+
Then, if the signal changes, all observers need to be re-evaluated.
730
+
*** Dependency Maintenance
731
+
- When evaluating a signal-valued expression, need to know which signal caller gets defined or updated by the expression
732
+
- if we know that, then executing a =sig()= means adding =caller= to the =observers= of =sig=.
733
+
- When signal =sig='s value changes, all previously observing signals are re-evaluated and the set =sig.observer= is cleared.
734
+
- Re-evaluation will re-enter a calling signal =caller= in =sig.observers=, as long as =caller='s value still depends on =sig=
735
+
*** Who's Calling?
736
+
One simple(simplistic?) way to do this is to maintain a global data structure referring to the current caller. (we will discuss and refine this later).
737
+
738
+
That data structure is accessed in a stack-like fashion because one evaluation of a signal might trigger others.
739
+
*** Stackable Variables
740
+
#+begin_src scala
741
+
class StackableVariable[T](init: T) {
742
+
private var values: List[T] = List(init)
743
+
def value: T = values.head
744
+
def withValue[R](newValue: T)(op: => R): R = {
745
+
values = newValue :: values
746
+
try op finally values = values.tail
747
+
}
748
+
}
749
+
#+end_src
750
+
You access it like this
751
+
#+begin_src scala
752
+
val caller = new StackableVar(initalSig)
753
+
caller.withValue(otherSig) {...}
754
+
... caller.value ...
755
+
#+end_src
756
+
*** Set Up in Object Signal
757
+
We also evaluate signal expressions at the top-level when there is no other signal that's defined or updated.
758
+
759
+
We use the "sentinel" object =NoSignal= as the =caller= for these expressions.
760
+
761
+
Together:
762
+
#+begin_src scala
763
+
object NoSignal extends Signal[Noting](???) {
764
+
override def computeValue() = ()
765
+
}
766
+
767
+
object Signal {
768
+
private val caller = new StackableVariable[Signal[_]](NoSiganl)
769
+
def apply[T](expr: => T) = new Signal(expr)
770
+
}
771
+
#+end_src
772
+
*** The Signal Class
773
+
#+begin_src scala
774
+
class Signal[T](expr: => T) {
775
+
import SIgnal._
776
+
private var myExpr: () => T = _
777
+
private var myValue: T = _
778
+
private var observers: Set[Signal[_]] = Set()
779
+
update(expr)
780
+
781
+
protected def update(expr: => T): Unit = {
782
+
myExpr = () => expr
783
+
computeValue
784
+
}
785
+
786
+
protected def computeValue(): Unit = {
787
+
myValue = caller.withValue(this)(myExpr())
788
+
if (myValue != newValue) {
789
+
myValue = newValue
790
+
val obs = observers
791
+
observers = Set()
792
+
obs.foreach(_.computeValue())
793
+
}
794
+
}
795
+
796
+
def apply() = {
797
+
observers += caller.value
798
+
assert(!caller.value.observers.contain(this), "cyclic signal definition")
799
+
myValue
800
+
}
801
+
}
802
+
#+end_src
803
+
*** Discussion
804
+
Use global state
805
+
806
+
One problem: use multiple signal expressions in parallel
807
+
*** Thread-Local State
808
+
- Thread-local state means that each thread accesses a separate copy of a variable
809
+
- It is supported in Scala through calss =scala.util.DynamicVariable=.
810
+
#+begin_src scala
811
+
object Signal {
812
+
private var caller = new DynamicVariable[Signal[_]](NoSignal)
813
+
...
814
+
}
815
+
#+end_src
816
+
*** Another Solution: Implicit Parameters
817
+
Thread-local state still comes with a number of disadvantages:
818
+
- Its imperative nature often produces hidden dependencies which are hard to manage
819
+
- Its implementation on the JDK involves a global hash table lookup, which can be a performance problem
820
+
- It does not play well in situations where threads are multiplexed between several tasks.
821
+
A cleaner solution involves implicit parameters
822
+
- Instead of maintaining a thread-local variable, pass its current value into a signal expression as an implicit parameter.
823
+
- This is purely functional. But it currently requires more boilerplate than the thread-local soluiton
824
+
- Future versions of Scala might solve that problem
825
+
*** Summary
826
+
We only covered Discrete signals changed by events.
827
+
828
+
Some variants of FRP also treat continous signals.
829
+
830
+
Value in these systems are often computed by sampling instead of event propagation.
0 commit comments