Here's a bit of syntax for doing a kind of "functional reactive web programming." This example aims to implement the draggable list problem.
fun dragList(origWidget) : mouseIsDown => DOM -> DOM {
draggingElement = findHitElement(mousePos);
fun updateIndicator(widget) : mouseMoved => DOM -> DOM {
reorderNodes origWidget mousePos.y;
}
finally {
# updates the widget once the behavior's condition
# finally becomes false
reorderNodes origWidget draggingElement mousePos.y
}
}
This defines a "behavior" called dragList
which could be applied to a node in a DOM tree. The dragList widget has a type mouseIsDown => DOM -> DOM
. Here the small arrow ->
denotes an ordinary function type (arg on the left, result on the right). What comes before the big arrow (=>
) denotes a condition. This is something which must be true for the runtime system to invoke this function. So the type mouseIsDown => DOM -> DOM
says, "if the mouse is down, you can call this function with a DOM tree and expect another DOM tree." The runtime system will repeatedly pass the current DOM of the relevant widget and replace it with the result of the function.
You might apply the behavior to an HTML element something like this:
<ul l:behavior=dragList>
<li>Yorick</li>
<li>Horatio</li>
<li>Rosencrantz</li>
<li>Guildenstern</li>
</ul>
The same behavior can be attached to many page components this way. Conceivably, more than one component's behavior could be active at a given time. But within a behavior, internal functions should be serialized. In this system, a behavior can only destructively affect the component on which it is invoked—it follows from this that concurrent behaviors on distinct components cannot interfere. It remains to be seen whether every desirable GUI experience can be implemented this way.
Now, diving into the body of the dragList
behavior:
draggingElement = findHitElement(mouseDownPos);
The dragList behavior begins by letting draggingElement
refer to the element where the mouse is originally clicked; we'll use it later.
Next dragList
gives an internal function which has its own condition; as long as the outer block is active, the inner block stands to be called.
fun updateIndicator(widget) : mouseMoved => DOM -> DOM {
reorderNodes origWidget draggingElement mousePos.y
}
The inner function's condition only applies while the outer function's condition is true. So what this block expresses is that, while the outer condition is true, whenever the mouse moves, the runtime system should call updateIndicator
. In order to implement the nested conditions, the compiler will write code to register and unregister the inner function with the appropriate event handlers whenever the outer function's condition is satisfied/falsified.
Finally, the use of the inner function, like that of the outer function, is to update the component. The runtimes system passes the current DOM for the component and replaces it with whatever the function returns. In this way, we model changes over time without destructive operations.
Now to progress to the next level of sophistication, we can observe that the type annotations are unneccessary and the compiler should be able to derive both the argument types and also the conditions under which the function needs to be called. This works because there are special thunks which refer to environmental variables that can change. Omitting the type annotations from the original figure, we get:
fun dragList(origWidget) {
draggingElement = findHitElement(mouseDownPos);
if (not(childOf(origWidget, draggingElement)))
return origWidget
fun updateIndicator(widget) {
reorderNodes origWidget draggingElement mousePos.y
}
finally {
# updates the widget once the behavior's condition
# finally becomes false
reorderNodes origWidget draggingElement mousePos.y
}
}
the compiler could still determine, making use of referential transparency, that updateIndicator
will not return a distinct result unless the value of mousePos
has recently changed. Thus it can infer the condition type of mouseMoved
and behave as before, registering the function with the mouseMoved
event at the javascript level. Similarly, the outer function, dragList
, should be invoked whenever the mouseDownPos
special thunk has changed its value. In fact, the value returned will not change as long as draggingElement
is a null value—that is, as long as mouseDownPos
lies outside the elements within this widget, and findHitElement
returns some node which is not a part of this widget. (In fact this explicit test is ugly, and in a moment I'll look at ways of eliminating it.)
This can be seen as a declarative approach to web GUIs, because each function, inner or outer, is essentially just "giving the value" of the widget as a function of some environmental conditions. Besides the mouse button and position, other environmental variables could be used (keys down, wall-clock time, a changing signal from the server, etc.). These "special thunks" are analogous to what Hudak and Elliott call "signals" in the area of Functional Reactive Animation [1].
Now ideally, the compiler should determine when two inner functions of one behavior would compete—when their conditions overlap; then there would be a race to update the component, possibly with different values. This is a mistake and the programmer should be prompted to change the code so that only one value of the widget is implied by any given set of environmental changes. Perhaps this derivation could be by means of a condition type on the special thunks, and a typing rule which bubbles these conditions up to any containing expressions.
Lexical scoping is an important factor here, which needs to be pinned down. In this case the inner function was a function of origWidget
, that is, the original structure of the component—rather than its own paramter, widget
, which would give the current structure of the widget at the time of invocation. This is just a design choice for the behavior-designer; under other circumstances it may be more natural to use the current widget
instead. Of course, the runtime system should take care not to destroy the lexically-scoped origWidget
value while this behavior is active.
A lot needs to be pinned down. The condition types have been presented a bit too breezily. After all, mouseIsDown
has a clear truth value at any moment; but mouseMoved
describes an instantaneous change in a sort of continuous variable. Some care should be taken in defining these conditions. The question of how to handle an instantaneously-changing variable and its downstream effects has been looked at in functional reactive programming.
Also, a bit more expressivity would be useful. Above, we would prefer to describe the condition of the dragList
function more finely as: "mouse is down and its original hit point is within me." This calls for some way to parameterize the condition type, e.g. "mouseIsDown && firstHit(insideMe)." It's not obvious that all the natural conditions and parameters will be easily and soundly expressible.
Finally, in talking to Phil about some of these ideas, he suggested that rather than operate on successive values for the view of the widget, the behavior functions should instead operate on a model of the widget; another set of viewing functions should be automatically invoked to transform a model into its realization in DOM form. I think this is a good idea and I need to explore this further.
[1] Elliott, C. and Hudak, P. 1997. Functional reactive animation. In Proceedings of the Second ACM SIGPLAN international Conference on Functional Programming (Amsterdam, The Netherlands, June 09 - 11, 1997). A. M. Berman, Ed. ICFP '97. ACM Press, New York, NY, 263-273.