| croczilla.com | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
An introduction to OniMotivationManaging the control flow of concurrent systems such as web applications, interactive UIs, or communication software is a notoriously difficult problem. Common patterns such as asynchronous callbacks or continuation passing can quickly become unwieldy and lead to unmaintainable 'spaghetti' code. Threads (be they cooperative micro-threads as implemented in e.g. Stackless Python or proper operating system threads) can help by allowing us to transform hairy asynchronous code into easier-to-read blocking code, but at the same time they make it more difficult to coordinate the overall action. The root of the problem is that conventional control structures (loops, conditionals, exceptions, ...) don't extend across threads and that threads lack a quality of composability. E.g. imagine you have a number of threads that perform some network actions. How could you retrofit this arrangement with a timeout that abandons the threads after a set period of time? Can you do it without editing the code of the individual threads? The idea behind Oni is to make these sort of transforms as easy as they are in conventional sequential code: Par(Action1(), Action2(), Action3()) performs the three actions in parallel and blocks until they all return. Alt(Timeout(1000), Par(Action1(), Action2(), Action3())) returns when either Timeout or Par return, whichever returns sooner. As another example, imagine that we have a 'heartbeat thread' that returns when it detects some network traffic. We want to reset the timeout every time we get a heartbeat. In Oni, this is easy to formulate:
Alt(Loop(Alt(Seq(Timeout(1000), Break()),
Heartbeat())),
Par(Action1(), action2(), action3()))
A working Oni exampleTo illustrate how Oni can be used in a web application setting, let's assume we want to write a form that performs a Google search and take it through a couple of stages of development, enhancing the functionality at each stage. Stage 1Let's start with a skeleton webpage with a text field, a search button and a div that will hold the search results:
<html>
<head>
<script type="text/javascript">
function init() {
var query_elem = document.getElementById("query");
var go_button = document.getElementById("go");
var results_elem = document.getElementById("results");
/* XXX */
}
</script>
</head>
<body onload="init()">
<h1>Oni example 1</h1>
<h2>Simple Google search</h2>
<input id="query" type="text"></input>
<input type="button" value="Search" id="go"></input>
<br></br>
<div id="results"></div>
</body>
</html>
To load the Google search API and the Oni framework code, we use something like this:
<script type="text/javascript"
src="http://www.google.com/jsapi"></script>
<script type="text/javascript" src="oni.js"></script>
Now we'll need some helper code. First a function for clearing all children of a given DOM node. (We'll use this to clear old results from the results div). Since we want to use this function within an Oni expression, we convert it into an Oni function using the Oni SLift ("synchronous function lift") operator:
var ClearChildren = SLift(
function(node) {
while (node.firstChild)
node.removeChild(node.firstChild);
}
We'll also need a function to access the value of a member of a JavaScript object:
var Member = SLift(function(obj, mem) { return obj[mem]; });
Next, we want an Oni function that allows us to wait for a given DOM event (such as the search button being pressed). We can construct this from a JavaScript function using Oni's ALift ("asynchronous function lift") operator:
var WaitForDOMEvent = ALift(
function(cont, event, element) {
function listener(e) {
cancel();
cont([true, e]);
};
function cancel() {
if (element.removeEventListener) // FF, Safari, Chrome
element.removeEventListener(event, listener, true);
else // IE calls it 'detachEvent'
element.detachEvent("on"+event, listener);
}
if (element.addEventListener) // FF, Safari, Chrome
element.addEventListener(event, listener, true);
else // IE calls it 'attachEvent'
element.attachEvent("on"+event, listener);
return cancel;
}
);
In a similar way we construct a couple of wrappers for the Google search API. One to initialize the search service:
var InitGoogleSearchService = ALift(
function(cont) {
google.load("search",
"1",
{"callback" : function() {cont([true, null]); } });
return null;
}
);
And one to perform a simple search for the query string 'query', with the output being appended to 'target_node':
var SimpleGoogleWebSearch = ALift(
function(cont, query, target_node) {
var s = new google.search.WebSearch();
var cancelled = false;
function completion() {
if (cancelled) return;
for (r in s.results) {
if (s.results[r].html)
target_node.appendChild(s.results[r].html.cloneNode(true));
}
cont([true, null]);
}
s.execute(query);
s.setSearchCompleteCallback(window, completion);
return function() {
cancelled = true;
}
}
);
Now for the main program logic. We want to initialize the search service, wait for a click on the search button, and perform a search for the string typed into query_elem. And when we're done, we want to wait for a click again, and so forth. And we want to make sure results get cleared before appending anything new to results_elem. Expressed as an Oni program:
var main =
Seq(InitGoogleSearchService(),
Loop(WaitForDOMEvent("click", go_button),
ClearChildren(results_elem),
SimpleGoogleWebSearch(Member(query_elem, "value"),
results_elem)
)
);
Here, Seq is a sequence of Oni expressions that will be executed one after the other. Loop is an endless loop. And to run it: Eval(main); Stage 2Our search form seems to work pretty well, but it has a slight problem: The UI (i.e. the 'Search' button) is ineffective while the search is being performed. So if we accidentally search for "endoplamatic" instead of "endoplasmatic", we have to wait for the current erroneous search to complete before searching again. Google search is pretty snappy, so the problem isn't really apparent. See here for a version of the Stage 1 code that slows the Google search to 5 seconds: example-01-slow.html. What we really want to do is to wait for a button press at the same time as waiting for completion of the search. If a button press occurs, we want to restart the search anew:
Loop(Alt(Seq(ClearChildren(results_elem),
SimpleGoogleWebSearch(Member(query_elem, "value"),
results_elem),
Stop()
),
WaitForDOMEvent("click", go_button)
)
)
Here, the Alt operator executes its children (the Seq(...) and the WaitForDOMEvent()) concurrently and returns as soon as either of them returns. Now, as the Seq(...) terminates with the expression 'Stop()', it never returns, so the Alt will only be exited when the button is pressed. In which case we go around the loop again, starting a search and waiting for a button press at the same time. We still need to wait for a button press before entering the loop, so the complete code for our 'improved' version looks something like this:
var main =
Seq(InitGoogleSearchService(),
WaitForDOMEvent("click", go_button),
Loop(Alt(Seq(ClearChildren(results_elem),
SimpleGoogleWebSearch(Member(query_elem, "value"),
results_elem),
Stop()
),
WaitForDOMEvent("click", go_button)
)
)
);
Again, here is a version that slows the Google search to 5 seconds: source: example-02-slow.html. Here are a couple of alternative formulations of the stage 2 code: - Using functional abstraction - Using macro abstraction (JS as a macro language for Oni) Stage 3Let's add a new feature to the Search form: We want the search to commence as soon as something is typed into the edit field. In other words, in addition to triggering the search by a 'click' event on the search button, we also want it to be triggered by a 'keypress' event on query_elem. This is as simple as replacing WaitForDOMEvent("click", go_button) by Alt(WaitForDOMEvent("click", go_button), WaitForDOMEvent("keypress", query_elem)). Since the WaitForDOMEvent() appears twice in the program, let's also factor it out into a new function. The complete code then is:
var WaitForTrigger = Defun([],
Alt(WaitForDOMEvent("click", go_button),
WaitForDOMEvent("keypress", query_elem)
)
);
var main =
Seq(InitGoogleSearchService(),
WaitForTrigger(),
Loop(Alt(Seq(ClearChildren(results_elem),
SimpleGoogleWebSearch(Member(query_elem, "value"),
results_elem),
Stop()
),
WaitForTrigger()
)
)
);
Stage 4As a final refinement of our code, let's introduce a delay before we allow a keypress to trigger a search. With WaitForTrigger redefined in the following way, searches will only be 'auto' triggered after the user has stopped typing for 1 second:
var WaitForTrigger =
Defun([],
Alt(WaitForDOMEvent("click", go_button),
Seq(WaitForDOMEvent("keypress", query_elem),
Loop(Alt(Delay(1000, Break()),
WaitForDOMEvent("keypress", query_elem)
)
)
)
)
);
|
| (c)2005-2009 alex fritze | |