Wednesday, December 26, 2012

Reactive Objects and Arrays via Direct Proxies

Arrays are a crucial building block for programming. Consequently, an essential question for functional reactive programming is how to react to imperative array updates. Designing a clean interface gets bogged down in issues like identity, API consistency, and efficient incrementalization. I used a recent extension to JavaScript, Direct Proxies (Firefox-only),  to sketch a transparent dual-mode model (JSFiddle) for Flapjax. I made a reactive array interface that covers the entire Array interface and supports both instant and batched updates. A fun holiday break from Superconductor :)

Update: I generalized the code in two ways: it works with object instances and also allows choosing whether the prototype is Array or Behavior.

Transparency


The idea is to create a reactive array that is tied to an existing normal array:

var arr = ['init'],
var arrB = bindB(arr);

From then on, interactions with one array are automatically reflected from one to the other. (In particular,  arrB uses arr as its backing store and proxies interactions to it):

// assert(arr[0] == arrB[0]);
arr.push('a');  // assert(arr.length == fxArr.length);
arrB.push('b'); // assert(arr.length == fxArr.length);


By defining traps for JavaScript's meta-object proxy protocol, I guarantee that all of JavaScript's array methods are supported by the reactive array arrB. All in all, for normal JavaScript, it should not matter if you pass around arr or arrB: they act on the same underlying data.

(Full transparency might be wrong. I'll raise a semantic issue at the end about what if you *do* want to distinguish reactive arrays vs. flat ones using operators such as instanceof.In my implementation, I let you pick as an optional parameter to bindB.)

Functional Reactivity and Instant Reactions


Now the cool thing is that array mutations can be automatically reacted to using normal functional reactive programming constructs.

For example, we can declare a live counter of the array size:

insertDomB(arrB.liftB(function (av) { return av.length; })); 

The above snippet is the usual functional reactive code that is a joy to read. It displays the length of the array. Whenever arrB updates, the GUI will automatically update to show the correct length. 

The new ability here is that wrapped array mutations automatically trigger reactions:

arrB[30] = 20; //updates length counter on screen
arrB.push('hello'); //updates length counter on screen

I'm calling these instant reactions because each mutation registers an update event. In the above statements, two update events will be fired, one for each statement.

The original array and reactive array refer to the same data, but differ on how they interact with the event system. Updates to the original array do not trigger reactions:

arr.push('world');  //no screen update


Maintaining non-triggering behavior of the original array is important so that non-reactive code not ready for the extended behavior does not trigger massive DOM updates. In places where it is safe to do the switch, arrB can be swapped in.


Batched Reactions

A nice pattern falls out of the wrapped and unwrapped view of the array. When many updates to an array should happen, we can batch them for a single subsequent response:

function addSome(n, arrB) {
  //batched update
  for (var i = 0; i < n; i++) {
     arrB.last.push(i);
  } 
  //notification
  if (n < 0) 
     arrB[0] = arrB.last[0]; //updates length counter on screen
}
addSome(10, arrB);

The "arrB.last.push(i)" calls trigger no reactions, while the assignment to arrB[0] triggers an instant reaction. This pattern of silent array updates followed by an instant reaction on the reactive view is a batched reaction. It is largely an optimization, but if you create local invariants inside the body of addSome, you can also use the pattern for  (non-nested) transactional reactions.


Object Instances and Meta Protocols

In response to Shriram's comment, the technique here is a late-bound dual to his work in making legacy classes reactive. For Racket's class system, he showed how to make a reactive subclass: the case of arrays would act like "var myArrB = new ArrayB(1, 2, 3); assert(myArrB instanceof Array)". The dual problem examined in my post is where an instance of a class must be made reactive ("var myArr = [1,2,3]; ...; var myArrB = bindB(myArr)"). It is an important practical distinction because the developer writing "var myArrB = bindB(myArr)" might not have control over "var myArr = [1,2,3]" nor "...".

His connection is important for two reasons: it provides a principle for how bound arrays should act (as subclass instances) and, more generally, shows that the same approach can be used for making object instances reactive.

1. Design principle of subclassing. Given an array, his connection tells us that reactive version should act as a subclass of Array. As seen above, this works for methods like push and [] (get). However, the desired object protocol behavior is not obvious. For example, should "assert(myArrB instanceof Array)"  succeed? By the subclass reasoning, yes!

Unfortunately, setting myArrB to be an instance of Array breaks introspection within the Flapjax library. To add dependencies to an object, Flapjax checks whether it can register event listeners. For example, "liftB(function (v) { return v.length; }, myArrB)" triggers a check "arg2 instanceof Behavior": if myArrB is an Array and not a Behavior, Flapjax will not register listeners on it! Some examples still work, such as  "myArrB.liftB(function (v) { return v.length; })", because the check is elided. For now, I support two constructor options:

var xB = bindB(arr);
assert(xB instanceof Array); //indistinguishable from Array
xB.liftB(function (v) { return v.length; }); //only supports chained reaction


var yB = bindB(arr, true);
assert(xB instanceof Behavior); //distinguishable from Array
yB.liftB(function (v) { return v.length; }); //chained reactions
liftB(function (v) { return v.length; }, yB); //and as arguments



The non-default constructor is more readily distinguished from an Array, and Flapjax uses this ability to  out-of-the-box to to react to it. If code consuming "myArrB" does not need "myArrB instanceof Array", I suggest the second, non-default form.


2. Generalization to Objects. The motivation of this post was to handle array instances, but as Shriram implies, those are a special form of object instances. We get the simpler case of objects for free!

var o = {};
var p = bindB(o);

insertDomB(DIV("o has x: ", p.liftB(function (v) { return v.x ? true : false; })));
//display shows false

p.x = 1; // o.x == 1 and display updates to show true
delete p.x; //o.x == undefined and display updates to show false

The code snippet above shows creating a reactive view of an object. The original object is the backing store for the view, and edits to the reactive view automatically trigger DOM updates. As with arrays, when reactions occur can be controlled by choosing to edit the raw object or the view.


Conclusion


Via proxies, I was able to make imperative arrays play nicely along their entire API with our FRP library and support both instant and batched/transactional updates.  You can play with this in Firefox via my JSFiddle. There is more to do (for example, the direct proxy API precludes behavioral indices  like fxArr[secondsB]), but the flexibility of this approach is handy.


APPENDIX


function bindB(objasObj//Behavior prototype by default

    function close(fthat{
        return typeof(f== 'function' ?
            function({
                return f.apply(thatarguments);
            
            f;
    }

    var resE receiverE();
    var resB resE.startsWith(obj);

    var protocol {
        setfunction(_pv{
            obj[pv;
            resE.sendEvent(arr);
        },
        getfunction(_p{
            return == "toSource" 
                close(obj.toSourceobj
            resB.hasOwnProperty(p
                resB[p
            Behavior.prototype.hasOwnProperty(p
                close(Behavior.prototype[p]resB
            obj[p];
        }
    };

    return new Proxy(asObj obj resBprotocol);
}
        
    
var arr ['init'];
var fxArr bindB(arr);


Post a Comment