Making promises in Tcl

Published , updated

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 Tcl error 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. The then_resolve and then_reject commands result in the promise returned by thenpromise being fulfilled or rejected with the values passed to those commands. The then_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 the then 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 and race)

  • 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.

Promises/A+ standard

Forbes Lindesay's site

JavaScript Promises by Jake Archibald

Mozilla Developer Network Promise reference

comments powered by Disqus