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.)
(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
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
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(obj, asObj) { //Behavior prototype by default
function close(f, that) {
return typeof(f) == 'function' ?
function() {
return f.apply(that, arguments);
}
: f;
}
var resE = receiverE();
var resB = resE.startsWith(obj);
var protocol = {
set: function(_, p, v) {
obj[p] = v;
resE.sendEvent(arr);
},
get: function(_, p) {
return p == "toSource" ?
close(obj.toSource, obj)
: resB.hasOwnProperty(p) ?
resB[p]
: Behavior.prototype.hasOwnProperty(p) ?
close(Behavior.prototype[p], resB)
: obj[p];
}
};
return new Proxy(asObj ? obj : resB, protocol);
}
var arr = ['init'];
var fxArr = bindB(arr);
2 comments:
This is interesting because it handles very nearly the dual case of compound imperative objects as our crossing-state-lines work:
http://cs.brown.edu/~sk/Publications/Papers/Published/ick-adapt-oo-fwk-frp/
Yes, I think your paper with Dan is actually one of the more important ones in FRP literature. (Namely, "how do we get this to work, for reals?!").
To paraphrase in terms of OO, the broad issue I'm having here is with instances (a preexisting array ['init']), rather than classes (Array) as in MrEd scenario. This relationship seems useful because I was struggling with basic design principles: is the reactive array instance a Behavior, an Array, both, ... what properties should the design guarantee?
Subclassing relationships might not quite work in the prototype world. In fact, it might not be right in the OO world once we care about things like reflection. I suspect 'Behavior' might be more of a mixin, or a prototype object with a parameterized base prototype (Array, Object, String, ... ).
Post a Comment