Reactive Programming with Reactive Variables
Christopher Schuster, Cormac Flanagan
University of California, Santa Cruz
{cschuste, cormac}@ucsc.edu

Example Application: A Stopwatch

Graphical User Interface

Internal State


Time: 0
{
  paused: false,
  time: 0
}

Stopwatch: Imperative Implementation


  var time = 0;
  var paused = false;

  setInterval(function() {
    if (!paused) {
      time++;
      $("#time").text("Time: " + time);
    }
  }, 1000);

  $("#button").click(function() {
    paused = !paused;
    $("#time").text(
      paused ? "Paused" : "Time: " + count);
  });
        

Stopwatch: Reactive Programming with RxJS


var pausedSig = Rx.Observable
  .fromEvent($("#button"), "click")
  .scan(function(paused) {return !paused;},false)
  .startWith(false);

var timeSig = Rx.Observable.interval(1000)
  .pausable(pausedSig)
  .scan(function(c) { return c + 1; }, 0)
  .startWith(0);

Rx.Observable.combineLatest(timeSig, pausedSig,
  function(time, paused) {
    return paused ? "Paused" : "Time: " + time;
  })
  .subscribe(function(s){$("#time").text(s);});

Libraries for Reactive Programming

Advantages

  • Provide data structures for creating and composing signals
  • No language extension or custom runtime necessary

Disadvantages

  • Programs consists of two kinds of values
    • "Normal" values, e.g. 23, "foo", [1,2,3]
    • "Reactive" values (signals/behaviors), e.g. stream of mouse clicks
  • Primitive operations need to be lifted to reactive values

Goal: Reactive Programming without Signals

Reactive Variable Updates


let i = 0;
let j = i + 1;

i = 3;
j = i + 1;
console.log(j);  // 14
        

rlet i = 0;
rlet j = i + 1;

i = 3;

console.log(j);  // 4
        

Normal assignments do not update dependent variables

Assignment to reactive variable i also updates dependent variable j

Stopwatch: Reactive Variables

rlet paused = initially(false)
              subscribe($("#button").click)
              !paused;

rlet time = initially(0)
            subscribe(interval(1000))
            paused ? time : time + 1;

rlet txt = paused ? "Paused" : "Time: " + time;

subscribe(txt) { $("#time").text(txt); }

Reactive Change Propagation:
1. Triggering a Reactive Update

Assigning new values with imperative push updates

rlet txt;
txt = "foo";

Alternatively: Using subscribe syntactic sugar to automatically trigger updates for JavaScript functions expecting a callback

rlet txt = subscribe(input.changed) input.text();

Reactive Change Propagation:
2. Propagating Changes to Dependent Variables

Reactive variable referenced by other reactive variables automatically set up a dependency

rlet len = txt.length;

Alternatively, subscribe can be used to denote a dependency without using the value

rlet lastChanged = subscribe(txt) Date.now();

A reactive variable reference in its own definition denotes the previous value to enable stateful computation

rlet numChanged = subscribe(txt)
                  initially(0) numChanged + 1;

Reactive Change Propagation:
3. Reacting to Updated Variables

Assignments might update dependent reactive variables which can be accessed like normal variables

console.log(numChanged);   // "23"
txt = "bar";
console.log(numChanged);   // "24"

Alternatively, a special subscribe syntax can be used to invoke imperative code at each change

subscribe(numChanged) { console.log("goo"); }
txt = "bar";    // "goo"

Demo (https://levjj.github.io/rlet)

Implementation (1/2)

rlet is a macro-generating macro (using the sweet.js macro system)

  • each reactive variable defines a scoped macro
  • assignments to the reactive variable trigger push updates on the underlying signal
  • normal references simply return the last/current value
  • implicit subscriptions due to reactive variable references require local expand, parse and scope analysis during macro expansion

Github Repository: https://github.com/levjj/rde/

Online Live Demo: https://levjj.github.io/rde/

Implementation (2/2)

rlet i = 0;



rlet j = i + 1;




i = 3;

console.log(j);
        
var S_i = new Sig(function(){
  return 0;
}, null);

var S_j = new Sig(function(){
  return IMM(S_i).read() + 1;
}, null);
S_i.onUpdate(S_j);

S_i.push(3);

console.log(S_j.read());

Side effects during reactive change propagation

Manipulating global state during change propagation results in additional, non-reactive data dependencies

var x = 1;
Rx.Observable.interval(100)
  .map(function() { return x * x; })  // x2 ≈ x*x
  .map(function(x2) { x++; return x2; });
  .subscribe(function(x2) { alert([x, x2]); });
                       // ≈ alert([x, x*x]); 
Output: [1,1], [2,4], [3,9], [4,16] ...
Output: [2,1], [3,4], [4,9], [5,16] ...

Prevent global state mutation

All references to surrounding variables in update expression of rlet are wrapped in IMM()

function IMM(x) {
  return new Proxy(x, {
    get: function(target, key) {
      return IMM(target[key]); },
    set: function() {
      throw new TypeError('immutable!'); }
  });
};

Formal Semantics

Implemented in JavaScript but applicable to any imperative language
Operational semantics

Discussion

Advantages

  • No reactive values, so no lifting required
  • Dependencies determined by lexical scope
  • Easy to reason about
  • Acyclic dependency graph
  • Can be re-evaluated without glitches

Disadvantages

  • Reactive variables cannot be reified and passed around as values
  • No higher-order reactive programming
  • Generic operations like filter cannot be implemented just in terms of reactive variables

Future Work

Static mapping of reactive dependencies to lexical scopes as basis for debugging and visualizations for reactive programming

Asynchronous updates of reactive variables

Continuous reactive variables that do not propagate updates with unchanged values

Thank you!

  /