Tuesday, December 4, 2007

Auto-Lifting Reactive Parameters, Snapshots

Now that our students have implemented a few functional reactive library combinators as well as a compiler to automatically lift functions, we're making them write a simple app. The infrastructure we provided already supports data binding template tags for them.

They must make make a fish-eye menu (think the OSX dock) where images are loaded from Flickr's public feed and rotate every few seconds. They should also display tags associated with the photos as well as some status text indicating that the page is waiting for the next feed between when a request is made and a response is received. My basic implementation of that took roughly 50 lines, including HTML, with maybe another 5-10 lines of that to be very clear about what the variables are, instead of the type of inlining I'd normally do. My new version of the above menu is significantly less hacky and more readable as it wasn't written at 2am this time around. The students need to go a bit further, and instead of using the general random public feed, synthesize a queue of search tags that can be added to, which drives the urls selected for feeds.

Three interesting things came up during this exercise:

Pure FRP

I did not have to go outside of the system to achieve functionality (language mode, not library) - I did not expect this. Their library is a significantly stripped down version of Flapjax, so I had to reimplement 2-3 effectively stdlib combinators, but those were all 1-2 lines of function body.

Trying to define a simple count function failed, linguistically

Consider


function count (n) {
return collect(n, function(acc,v) {return acc + 1;}, 0);
}
count(requests); //count the # of requests in the request stream


Collect is like fold (reduce), except instead of acting on a list of values that entirely exists, it acts on a stream, so as n gets new values, collect emits new values that are a function of that new value and the last previously computed value.

Count is seemingly written correctly: it gets an event stream, and passes it through collect. Collect will not be lifted because it is already defined to work in the world of reactive values. However, the function application "count(requests)" will be, being transformed into "lift(count, requests)". Whenever requests gets a new value, the new value event will be sent to count - meaning the event stream isn't passed. Instead, there are repeated invocations of count on individual request values. Thus, we must annotate the function "count" as being defined to know about reactive values (more formally, specify that the first parameter of count is defined in the 1-arity reactive domain):


function count (n) {
return collect(n, function(acc,v) {return acc + 1;}, 0);
}
count.__doNotLift = true;
count(requests); //count the # of requests in the request stream


However, we're language designers, not function decorating hackers. What are we really trying to declare? The function count is defined to work not just on some 'n', but n such that they are reactive values. Thus, if we really must annotate, let's do it right:


function count ({n}) {
return collect(n, function(acc,v) {return acc + 1;}, 0);
}
count(requests); //count the # of requests in the request stream


The interesting thing here is a function like delay, that takes a value (say the mouse position), and the amount of time to delay any change to it (for example, 500ms). It would be natural to define it as


function delay({val}, amt) {
//something with setInterval
}
var slowMouse = delay(mouse, 5);


However, what happens when the amount we want to delay is also variable? That's when the beauty of the parameter decoration comes in to play:


function delay({val}, amt) {
//something with setInterval
}
var slowMouse = delay(mouse, seconds);


Delay is defined to work on a time varying value and a number. It is easy to lift this to work on two time varying parameters:


function delay2({val}, {amt}) {
lift(function(t){ return delay(val, t); }, amt).switch();
}
var slowMouse = delay(mouse, seconds);


We use lift to unbox the amount, so we could pass it to delay. However, that means we're getting a stream of streams, leading us to switch, which lowers it back to the desired stream. It works, but that's ugly and annoying to write.

The really cool thing here is that, if we use the decorators, our compiler can automatically generate delay2 and you never write it nor think about it - you only have to write as much as necessary for the interesting functionality, and the rest can be lifted automatically. If you annotate all your variables with their time-varying-ness, all of the conversions would be statically done, but if you only annotate function parameters to define on what domain the function works on wrt those individual parameters, the compiler will conservatively insert a version of lift that could do any extra parameter-based lifting as needed. Similarly, you could imagine, instead of writing "__doNotLift", specify "__arity=[1,0]". If you want to be really dorky, imagine what happens when you have arity 2, 3, ... n.

That, in a nutshell, was my summer at Adobe research this year.

Showing the loading status was impressively declarative

Again, this is in the students' toy language, but neat nevertheless. Given a request stream to flickr and a response stream of photo feeds to be rendered, the students must show a loading bar while a request is sent but has not yet been heard from. I first made my status variable as follows:


var loading =
(count(requests) != count(responses));


As new request and response values go through the streams, we can see that a response is being loaded because the counts don't match up. Except... what about dropped requests? This is AJAX, so no error has occurred - the request has not yet timed out, but we're no longer really waiting for it anyway. If we make a new request and get a response back in the interim, we want to ignore that the first one ever happened, though if it did return, that'd be a plus.

The solution is a little longer, but made me happy:


var numResponses = count(responses);
var numResponsesAtLastRequest =
snapshot(requests, numResponses);
var loading =
(numResponses == numResponsesAtLastRequest);


Snapshot, on a trigger event (a request occurring), takes on the value of the second argument at that time (the number of responses we had at that point). Thus, we're loading when the number of responses hasn't changed since we made the last request.

Once they turn their homework in, I'll post my source (executable on our online compiler).
Post a Comment