Skip to content

Commit 68ce1d0

Browse files
committed
add week 2 lecture notes
1 parent 7b8edb2 commit 68ce1d0

File tree

1 file changed

+333
-0
lines changed

1 file changed

+333
-0
lines changed

reactive-programming.org

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,3 +495,336 @@ Call this the "bullet-proof" principle.
495495
Many of the types defining =flatMap= are monads. (If they also define =withFilter=, they are called "monads with zero").
496496

497497
The three monad laws give useful guidance in the design of library APIs.
498+
* Week 2
499+
** Functions and State
500+
*** Observation of Subsititution
501+
Rewriting can be done anywhere in a term, and all rewritings which terminate lead to the same solution.
502+
503+
This is an important result of the \lambda -calculus, the theory behind functional programming.
504+
*** Stateful Objects
505+
One normally describes the world as a set of objects, some of which have state that *changes* over the course of time.
506+
507+
An object *has a state* if its behavior is influenced by its history.
508+
*** Implementation of State
509+
Every form of mutable state is constructed from variables.
510+
511+
A variable definition is written like a value definition, but with the keyword =var= in place of =val=:
512+
#+begin_src scala
513+
var x: String = "abc"
514+
var count = 111
515+
516+
x = "hi"
517+
count = count + 1
518+
#+end_src
519+
*** State in Objects
520+
In practice, objects with state are usually represented by objects that have some variable members.
521+
#+begin_src scala
522+
// Example: Here is a class modeling a bank account.
523+
class BankAccoount {
524+
private var balance = 0
525+
def deposit(amount: Int): Unit = {
526+
if (amount > 0) balance = balance + amount
527+
}
528+
def withdraw(amount: Int): Int =
529+
if (0 < amount && amount <= balance) {
530+
balance = balance - amount
531+
balance
532+
} else throw new Error("insufficient funds")
533+
}
534+
#+end_src
535+
The class =BankAccount= defines a variable =balance= that contains the current balance of the account.
536+
537+
Note that =balance= is private in the =BankAccount= class, it therefore cannot be accessed from outside the class.
538+
539+
To create bank accounts, we use the usual notation for object creation:
540+
#+begin_src scala
541+
val account = new BankAccount
542+
#+end_src
543+
*** Statefulness and Variables
544+
** Identity and Change
545+
#+begin_src scala
546+
val x = E; val y = E
547+
val x = E; val y = x // y == x == E
548+
#+end_src
549+
*referential transparency*
550+
*** Operational Equivalence
551+
The precise meaning of "being the same" is defined by the property of *operational equivalence*.
552+
553+
In a somewhat informal way, this property is stated as follows.
554+
555+
Suppose we have two definitions =x= and =y=.
556+
557+
=x= and =y= are operationally equivalent if *no possible test* can distinguish between them.
558+
*** Testing Operational Equivalence
559+
f(x, y) == f(x, x)
560+
** Loops
561+
*** Definition of =while=
562+
The function =WHILE= can be defined as follows:
563+
#+begin_src scala
564+
def WHILE(condition: => Boolean)(command: => Unit): Unit =
565+
if (condition) {
566+
command
567+
WHILE(condition)(command)
568+
}
569+
else ()
570+
#+end_src
571+
*Note:* The condition and the command must be passed by name so that they're reevaluated in each iteration.
572+
573+
*Note:* =WHILE= is tail recursive, so it can operate with a constant stack size.
574+
*** Translation of For-Loops
575+
For-loops translate similarly to for-expressions, but using the =foreach= combinator instead of =map= and =flatMap=.
576+
577+
=foreach= is defined on collections with element of type =T= as follows:
578+
#+begin_src scala
579+
def foreach(f: T => Unit): Unit = ...
580+
#+end_src
581+
** Extended Example: Discrete Event Simulation
582+
Here's an example that shows how assignments and higher-order functions can be combined in interesting ways.
583+
584+
We will construct a digital circuit simulator.
585+
586+
The simulator is based on a general framework for discrete event simulation.
587+
** Imperative Event Handling: The Observer Pattern
588+
The Observer Pattern is widely use when views need to react to changes in a model.
589+
590+
Variants of it are also called
591+
- publish/subscribe
592+
- model/view/controller (MVC)
593+
594+
+------+ publish +-------+
595+
| |<----------+ |
596+
| view | subsribe | model |
597+
| +---------->+ |
598+
+------+ +-------+
599+
#+begin_src scala
600+
trait Publisher {
601+
private var subscribers: Set[Subscriber] = Set()
602+
603+
def subscribe(subscriber: Subscriber): Unit =
604+
subscribers += subscriber
605+
606+
def unsubscribe(subscriber: Subscriber): Unit =
607+
subscribers -= subscriber
608+
609+
def publish(): Unit =
610+
subscribers.foreach(_.handler(this))
611+
}
612+
613+
trait Subscriber {
614+
def handler(pub: Publisher)
615+
}
616+
#+end_src
617+
*** Observer Pattern, The Bad
618+
- Forces imperative style, since handlers are Unit-typed
619+
- Many moving parts that need to be co-ordinated
620+
- Concurrency makes things more complicated
621+
- Views are still tightly bound to one state; view update happens immediately
622+
To quantify (Adobe presentation from 2008)
623+
- 1/3 of the code in Adobe's desktop applications is devoted to event handling
624+
- 1/2 of the bugs are found in this code
625+
626+
** Functional Reactive Programming
627+
*** What is FRP?
628+
Reactive programming is about reacting to sequences of /events/ that happen in /time/.
629+
630+
Functional view: Aggregate an event sequence into a /signal/.
631+
- A signal is a value that changes over time
632+
- It's represented as a function from time to the value domain
633+
- Instead of propagating updates to mutable state, we define new signals in terms of existing ones
634+
*** Example: Mouse Positions
635+
- Event-based view:
636+
Whenever the mouse moves, an event
637+
#+begin_src scala
638+
MouseMoved(toPos: Position)
639+
#+end_src
640+
is fired.
641+
- FRP view:
642+
A singal,
643+
#+begin_src scala
644+
mousePosition: Signal[Position]
645+
#+end_src
646+
which at any point in time represents the current mouse position.
647+
*** Origins of FRP
648+
FRP started in 1997 with the papter /Functional Reactive Animation/ by Conal Elliott and Paul Hudak and the =Fran= library.
649+
650+
There have been many FRP systems since, both standalone languages and embedded libraries.
651+
652+
Some examples are: Flapjax, Elm, Bacon.js, React4J.
653+
654+
Event streaming dataflow programming systems such as Rx, are related but the term FRP is not commonly used for them.
655+
656+
We will introduce FRP by means of a minimal class, =frp.Signal= whose implementation is explained at the end of this module.
657+
658+
=frp.Signal= is modelled after =Scala.react=, which is described in the papter /Deprecating the Observer Pattern/.
659+
*** Fundamental Signal Operations
660+
There are two fundamental operations over signals:
661+
1. Obtain the value of signal at the current time. In our library this is expressed by () application.
662+
#+begin_src scala
663+
mousePosition() // the current mouse position
664+
#+end_src
665+
2. Define a signal in terms of other signals. In our library, this is expressed by the Signal constructor.
666+
#+begin_src scala
667+
def inReactangle(LL: Position, UR: Position): Signal[Boolean] =
668+
Signal {
669+
val pos = mousePosition()
670+
LL <= pos && pos <= UR
671+
}
672+
#+end_src
673+
*** Constant Signals
674+
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

Comments
 (0)