Making promises in Tcl
This post is obsoleted by the promise package (based on the code is this post) and by newer posts on the topic. Nevertheless it may still hold some tutorial benefit.
There is quite a bit of hoopla in the Javascript world around the promise abstraction for asynchronous code. Implemented in various forms in third party libraries, it proved sufficiently useful to be formally defined and incorporated into ECMAScript 6.
Other languages, from Python and Scala to C#, C++ and Java, have implementations of promises in various flavors (sometimes separated into promises and futures). Not finding one for Tcl (we cannot let Tcl lag!), I started on an experimental implementation described in this post.
Introduction
So what is this promise abstraction? In a nutshell, it provides a means to write code that runs asynchronously in a sequential style. It is easiest to illustrate with an example.
Let us assume you monitor many web sites and periodically need to check for liveness and print the result. Doing so in synchronous fashion, checking each in turn is straightforward.
But suppose you wanted to do the checks in parallel for reasons of latency and response time. You might try writing code for this purpose yourself. Below, we illustrate how you would write it using promises.
We will be using the standard http
library and the lambda
package from tcllib
for convenience (this is just a helper for defining anonymous procedures).
package require http
package require lambda
source ../../static/scripts/promises.tcl
The first step is to write a procedure that will act as the callback for the asynchronous version of http::geturl
and update a promise with the status result of the http request.
proc http_callback {prom http_tok} {
upvar #0 $http_tok http_state
$prom resolve [list $http_state(url) $http_state(status)]
::http::cleanup $http_tok
}
The code should be self-evident. The resolve
method updates the value of a promise.
Next, we need a procedure that wraps the async version of the geturl
command.
proc geturl_async {url prom} {
http::geturl $url -method HEAD -command [list http_callback $prom]
}
Then we define a procedure that will return a promise constructed from the above.
proc checkurl {url} {
return [promise::Promise new [list geturl_async $url]]
}
Now we can use this to check if Google is alive.
% set prom [checkurl http://www.google.com]
�?? ::oo::Obj36
This gives us a TclOO object promise which will eventually contain the result from the async call. How do we get know when the result is available? We register a callback that will run when the result is available, in this case to simply print the result.
% $prom done puts
Assuming you are running the Tcl event loop, you will see the following printed out when the operation actually completes.
http://www.google.com/ ok
This seems like a whole bunch of nothing that could have been as easily done directly with the -command
option to geturl
so why promises?
What makes promises different is that you can compose new promises by combining promises in various ways.
For example, going back to our original problem, we want to run concurrent checks for multiple URL's and print the results when all are done. We do this by constructing a new promise that is resolved when the promises for all the URL's complete. The promise::all
lets us do exactly this and can be used to define a procedure for the purpose.
proc checkurls {urls} {
return [promise::all [lmap url $urls {checkurl $url}]]
}
The checkurls
procedure collects the lists of promises corresponding to each URL and uses the promise::all
command to construct a new promise which will be resolved when all the contained promises are resolved.
set check [checkurls [list http://www.google.com http://127.0.0.1:1234 \
http://www.yahoo.com]]
$check done puts
The URL checks proceed in parallel and when all are completed you should see something like the following printed.
{http://www.google.com/ ok} {http://127.0.0.1:1234/ error} {http://www.yahoo.com/ ok}
It is this ability to compose asynchronous operations using sequential operations like lmap that make promises convenient to use.
Let us go even further. We would like to add a timeout which will abort the operation if all URL's do not resolve within a specified time. Our modified checkurls
procedure will now look like this.
proc checkurls {urls} {
set all [promise::all [lmap url $urls {checkurl $url}]]
set timer [promise::timer 100 "Operation timed out"]
return [promise::race [list $all $timer]]
}
We have added a timer based promise and then combined it with the master url promise using promise::race
. This returns a new promise that is resolved when any of the contained promises is resolved. Consequently, you can now do
set check [checkurls [list http://www.google.com http://127.0.0.1:1234 \
http://www.yahoo.com]]
$check done puts
Then depending on what happens first, you should see either the output shown previously or the message from the timer.
Operation timed out
Implementing this without promises would have been both trickier and less transparent.
We have defined multiple procedures above. Really, once you are comfortable with the use of promises, checkurls
could have been defined using anonymous procedures as
proc checkurls {urls} {
return [promise::all [lmap url $urls {
promise::Promise new [lambda {url prom} {
http::geturl $url -method HEAD -command [lambda {prom tok} {
upvar #0 $tok http_state
$prom resolve [list $http_state(url) $http_state(status)]
::http::cleanup $tok
} $prom]
} $url]
}]]
}
Notice that converting an API (http::geturl
in our example) to a promise-based one takes some code. But it is not a whole lot of code and more important, it is specific to the API, not the application. Thus when common API's are wrapped into promise-based versions (as is happening in other languages), they can be directly used.
You should have already gotten a feel for the power of this abstraction.
What is a promise
Having seen an example, we describe it in some more detail.
A promise encapsulates an asynchronous operation for which a result is expected at some point, which may be immediate or in the future. The result may come from a successful completion of the operation or from an error. Correspondingly, from the application's perspective a promise may be in one of three states:
-
in a
PENDING
state, the promise is still to awaiting completion of the asynchronous operation. -
in a
FULFILLED
state, the promise contains the result from a successful completion of the operation. -
in a
REJECTED
state, the promise contains the error result from a failure of the operation.
A promise starts out in a PENDING
state. It transitions to a FULFILLED
state when it is resolved with a value returned by the successful completion of the asynchronous operation. Alternatively, it transitions to a REJECTED
state when it is rejected with an error value from a failure in the operation. These two transitions are collectively called settling the promise. Once a promise is settled, it is frozen and neither the state or the contained value can change. Attempts to further update the promise are silently (by intention) ignored.
Creating a promise
Creating a promise requires writing some code to link an asynchronous API to the promise being created. This piece of code is passed to the promise constructor as a command prefix and is invoked by the it. It is responsible for initiating the asynchronous operation as well as arranging for the promise to be updated with the result. An example was the geturl_async
command we wrote earlier show again below.
proc geturl_async {url prom} {
http::geturl $url -method HEAD -command [list http_callback $prom]
}
promise::Promise create url_promise [list geturl_async $url]
An important point to note is that all errors from invoking this initializing code are trapped by the promise constructor and returned via the usual error handling mechanisms discussed below. This permits both synchronous and asynchronous errors to handled in the same uniform manner.
Settling a promise
A promise is settled in one of two ways:
-
Calling the
resolve
method of the promise resolves it with the value passed to the method. -
Calling the
reject
method of the promise rejects it with the error value passed to the method. By convention the error value should be a pair consisting of the error message and the error dictionary as created by the Tclerror
command.
In our example http_callback
code, we were only interested in the status and a failure to retrieve the URL was not treated as an error. Thus we only used the resolve
method.
proc http_callback {prom http_tok} {
upvar #0 $http_tok http_state
$prom resolve [list $http_state(url) $http_state(status)]
::http::cleanup $http_tok
}
If we wanted to treat the unavailability of the URL as an error as opposed to a status indication, we could have written the command as
proc http_callback {prom http_tok} {
upvar #0 $http_tok http_state
if {$http_state(status) eq "ok"} {
$prom resolve [list $http_state(url) $http_state(status)]
} else {
$prom reject [list $http_state(url) $http_state(status)]
}
::http::cleanup $http_tok
}
Unavailable states would then be reported as errors via error handlers as we discuss later.
Waiting for results
Once a promise is constructed, the application has to arrange for the code that uses the computed results to run. This is accomplished with the done
method call that we saw in our earlier example.
$prom done puts
The done
method takes a command prefix to be invoked when the promise is resolved. This command prefix is invoked with an additional argument - the value of settled promise. In our example, we are just interested in printing the result so simply pass puts
as the command. In the general case where something more needs to be done with the result, you would invoke done
as
$prom done [lambda {val} {
...Some code to process val...
}]
The done
method may be called at any time irrespective of whether the promise is settled or not. If the promise is still in the pending state, the specified callback will be invoked when the promise is settled. If the promise has already been settled, the callback is invoked right away. In both cases, the callback is invoked via the event loop. Applications therefore do not have to worry that the callback will occur as part of the invocation of the done
method itself in the case the result is already available. (This behaviour is as specified by the ES6 standard for Javascript promises).
Note that the done
method may be called multiple times. The specified callbacks will all be queued and invoked appropriately. For example, the promise::geturl
procedure in the package returns a promise that resolves to the state dictionary returned by the http::geturl
command of the http
package. We could invoke done
on it multiple times to retrieve various parts of the state.
% set prom [promise::geturl http://www.google.com]
% $prom done [lambda {http_state} {puts [dict get $http_state url]}]
% $prom done [lambda {http_state} {puts [dict get $http_state http]}]
Error handling
So far we have implicitly described how to run code when a promise is successfully settled (fulfilled). How do we run code in response to the promise being rejected? It turns out that the done
method takes two arguments - the callback to invoke when the promise is resolved and the callback to invoke when it is rejected.
The general form of the done
method is
PROMISE done FULFILLHANDLER ?REJECTHANDLER?
When the promise is fulfilled, FULFILLHANDLER will be run via the event loop. When the promise is rejected, the REJECTHANDLER will be run in the same manner. Both handlers are optional in that they can be specified as the empty string (or just left out in the case of REJECTHANDLER).
Chaining operations
There are times when we want to chain multiple asynchronous operations where each operation is run after completion of the previous one. This might be because subsequent operations are dependent on the results of the prior ones, or we want to limit resource usage etc. For example, instead of running our URL checks in parallel we might want to run them one after the other sequentially. Note this is different from calling the synchronous version of geturl
because in that case we would block the entire application including the user interface while the sequence of operations was completed.
Chaining promises is done through the then
method. Let us see an example first using the checkurl
command we defined earlier, and then follow with an explanation.
% set prom1 [checkurl http://www.google.com]
�?? ::oo::Obj62
% set prom2 [$prom1 then [lambda {val} {
puts $val
promise::then_promise [checkurl http://www.yahoo.com]
}]]
�?? ::oo::Obj63
http://www.google.com/ ok
% $prom2 done puts
�?? http://www.yahoo.com/ ok
Although not obvious from the above shell session, the two URL checks run asynchronously but in sequential fashion, the second one being triggered when the first completes.
So how does the above work? The then
method is similar to the done
method we saw earlier except that while done
method simply runs the appropriate fulfillment or rejection handler, the then
method additionally constructs a new promise and returns it. The code fragment with explanatory notes is shown below.
% set prom2 [$prom1 then [lambda {val} { <1>
puts $val <2>
promise::then_promise [checkurl http://www.yahoo.com] <3>
}]]
<1> The callback is passed the fulfilment result from the promise
<2> Simply print out the previous result
<3> Creates a new promise with checkurl and sets it as the result of the then method
We could extend this chain by calling the then
method on prom2
as well but here we simply print out its result using done
instead.
Like done
, then
also takes two arguments
PROMISE then FULFILLHANDLER ?REJECTHANDLER?
which are called when the promise is fulfilled or rejected respectively. If a handler is not specified (i.e. an empty string), the returned promise is fulfilled (or rejected) with the value of PROMISE. This means you can pass on errors to a common error handler.
set prom2 [$prom1 then FULFILHANDLER]
set prom3 [$prom2 then ANOTHERFULFILHANDLER]
$prom3 done "" ERRORHANDLER
In the above pseudo-code, if prom1
is rejected the prom2
will also be rejected since the then
method has no rejection handler specified and likewise prom3
. The error can be then trapped by the done
call. This ability to funnel errors from sequenced asynchronous operations into a common error handler is one of the benefits of the promise abstraction.
The then
method shares other characteristics of the done
method. It can be called at any time, either before or after the promise is settled. In either case, the appropriate handler will be invoked via the Tcl event loop.
Also, it can be called multiple times on a promise.
set prom2 [$prom1 then ...]
set prom3 [$prom1 then ...]
This is not the same as chaining we saw earlier. Here prom2
and prom3
are both chained to prom1
but have no other relation to each other. They are settled independently when prom1
is settled.
There is an additional consideration related to the then
method that does not arise with done
and that has to do with creation and settlement of the promise returned by the method. The various possibilities are
-
If no fulfillment (or rejection) handler is specified, then the new promise is created and fulfilled (or rejected) with the same value as the promise whose
then
method was invoked. -
The fulfillment or rejection handler can explicitly settle the promise returned by
then
by calling one of three commands. Thethen_resolve
andthen_reject
commands result in the promise returned bythen
promise being fulfilled or rejected with the values passed to those commands. Thethen_promise
command, which we saw in our earlier example, takes a new promise (generally created within the handler) and links it to the promise returned by thethen
command. -
If the handler does not explicitly settle the promise as above, it is resolved by the return value from the handler if it returns without raising an error. If it raises an error, the exception is caught and the promise is rejected with a value that is a pair consisting of the error message and the error dictionary.
Composing promises
One of the benefits of promises is the ability to compose them easily to construct new promises. Combining asynchronous operations to fan-out / fan-in, handle dependencies etc. is much easier with promises than with directly using callbacks. We have already seen three different mechanisms for this - all
, race
and then
- and related examples so we will not discuss them further.
Summary of benefits
So to summarise the benefits of the promise abstraction,
-
It allows combining of results from multiple parallel asynchronous operations (
all
andrace
) -
It allows composition of asynchronous operations wherein each operation leads to another asynchronous operation in effect transforming the results (
then
). -
It generalizes the mechanisms so that responses to asynchronous operations of different nature can be used in the same fashion. For example, we can easily fire off a Web page retrieval, a computation in some other Tcl thread,and an external grep program, all in parallel with some action to be taken when both complete. The mechanisms for this are exactly what we saw when combining URL checks, though the underlying operations are different.
-
Unifies error handling across multiple asynchronous operations in a manner similar to chaining of exception handlers with Tcl
try
command. -
Promises retain state and provide memoization as a fringe benefit.
It should go without saying that promises are not a panacea for async programming.
-
The biggest pitfall in async programming, where the state of the world changes between scheduling of the operation and its running, still exists with promises and is something an application has to be aware of.
-
Existing API's cannot be directly used. They have to be wrapped as we showed with the
geturl
command. However, this wrapping is not difficult and only needs to be done once for an API. The promise package itself will contain promise-based wrappers for common operations like sockets, http, exec, timers, threads, variable traces etc. -
Promises are not suitable for operations that are very granular or stream based. For example, reading a file one line at a time is not a good candidate for a promise-based solution though promises are well suited for reading the entire output of an exec'ed program.
Unfinished business
The reason promises is still only a blog post as opposed to a complete downloadable package is that there is still unfinished business to be taken care of (in addition to formal documentation and a test suite).
-
The API needs to be refined based on actual usage experience
-
Unlike Javascript, Tcl does not have garbage collection for objects. Thus the promise objects (which are TclOO objects) need to be explicitly destroyed by the application, something we have conveniently glossed over in this post. This is likely to be error-prone in more complex applications. Some ideas to alleviate this are being explored.
-
For convenience, common Tcl async API's need to be wrapped
-
Relationship with respect to coroutines needs to be looked at, both in terms of their interaction as well as whether they might simplify implementation of promises.
-
It seems some mechanism to cancel a promise (operation) would be desirable, but there are subtle issues therein (which is why the capability is not standardised in Javascript either)
In the meanwhile, you can download the version described in this post from here.
Epilogue
A Tcl package based on the original script accompanying this post is available from SourceForge.
References and credits
The following references were very useful in my understanding of promises. They will on likelihood provide a better understanding of promises than this post, language differences notwithstanding.