In a series of prior posts, I had introduced the promise
abstraction along with the promise
Tcl package and how it can greatly simplify certain forms of asynchronous
computation. As mentioned there, the Tcl package is roughly based
on the promise framework implemented in ES6 (ECMAScript, aka Javascript).
ES7 introduced the async
and await
functions which further simplify
asynchronous programming with promises in certain scenarios. Accordingly,
the Tcl promise
package has been updated to include the equivalent
commands. This post introduces their use.
We will start a slightly modified form of an example from
an earlier post to provide the motivation
for the async
/await
commands. What we want to be able to
do is to download a set of URL's into a directory, place them into a
.zip archive and then email them. The catch is that
the whole process of downloading, zipping and emailing must not
block our process while it is ongoing. It must happen in the
background while allowing our UI updates, user
interaction etc. to function normally.
Our promise-based solution uses the download
procedure defined
below.
NOTE: All code samples assume the promise
namespace is on the
namespace path so that the commands can be used without
qualification. Moreover, remember that promises require the event loop
to be running.
proc download {dir urls} {
return [all [lmap url $urls {
[pgeturl $url] then [lambda {dir url http_state} {
save_file $dir $url [dict get $http_state body]
} $dir $url]
}]]
}
This procedure will initiate download of the specified URL's in parallel and in the background without blocking. It returns a promise that is fulfilled when the downloads complete. If you do not follow the above, you will need to read the previous posts on promises.
Given the above procedure, we can write the sequence of background
operations as the zipnmail
procedure
below.
proc zipnmail {dir urls} {
set downloads [download $dir $urls]
set zip [$downloads then [lambda {dir dontcare} {
then_chain [pexec zip -r pages.zip $dir]
} $dir]]
set email [$zip then [lambda dontcare {
then_chain [pexec blat pages.zip -to someone@somewhere.com]
}]]
$email done [lambda {dontcare} {
tk_messageBox -message "Zipped and sent!"
}]
}
This procedure "chains" together a sequence of asynchronous steps — the download, zip and email — abstracted as promises and then displays a message box when it's all done. Again, refer to previous posts if you are lost.
If you do not appreciate the value of the promise abstractions, try to implement the same functionality (remember it has to be asynchronous) using plain old Tcl as suggested in previous posts.
However, we can do better now that we have async
and await
. Here
is the new and improved version:
async zipnmail {dir urls} {
await [download $dir $urls]
await [pexec zip -r pages.zip $dir]
await [pexec blat pages.zip -to someone@somewhere.com]
tk_messageBox -message "Zipped and sent!"
}
As you can see, this new procedure, defined using the async
command
and not proc
, is significantly simpler and more
readable than the previous version. The boilerplate of sequencing
asynchronous operations is encapsulated by the async
/ await
commands and the intent is clear. And although the flow looks
sequential, the execution is asynchronous and does not block the
application while the operations are being executed.
We can call it as follows:
set prom [zipnmail c:/temp/downloads {http://www.example.com http://www.magicsplat.com}]
$prom done
With that motivating example behind us, let us look at the async
and
await
commands in more detail.
async
commandThe async
command is identical in form to Tcl's proc
command,
taking a name as its first argument, followed by parameter definitions
and then the procedure body. Just like proc
, it results in a command
being created of the name passed as the first argument in the
appropriate namespace context as well as an anonymous procedure
defined using the supplied parameter definitions and body.
When the created command is invoked, it instantiates a hidden coroutine context and executes the anonymous procedure within that context passing it the supplied arguments. The return value from the command is a promise. This promise will be fulfilled with the value returned from the procedure if it completes normally and rejected with the error information if the procedure raises an error.
Let us start with an example that has no asynchronous operations.
async demo {a b} {
return [expr {$a+$b}]
}
The above defines a procedure demo
. However, calling the procedure
does not return the concatenation of the two arguments. Rather, it
returns a promise that will be fulfilled when the supplied body
(run within a coroutine context) completes. In this case, there is no
asynchronicity involved so it will complete immediately. We can try
it out thus in a command shell.
% set prom [demo 40 2]
::oo::Obj70
% $prom done puts
42
Similarly, in case the command failed, the promise would be rejected as discussed in a previous post. For example, if we tried to pass non-numeric operands, the promise is rejected:
% set prom [demo a b]
::oo::Obj71
% $prom done puts [lambda {msg error_dict} {puts $msg}]
can't use non-numeric string as operand of "+"
Of course there is really no reason to use async
above because there
are no blocking operations that we want to avoid. So we will look at
another example where we add asynchronous tasks to
avoid blocking the whole application.
The example below simulates a "complex" computation. The result of the first computation is used in the second so there is a dependency that implies sequential execution between the two. However, remember we do not want to block the application so these two sequential steps need to be run asynchronously. The asynchronous procedure would be defined as follows.
async demo {a b} {
set p1 [ptask "expr $a * 10"]
set p2 [$p1 then [lambda {b val} {then_chain [ptask "expr $b + $val"]} $b]]
async_chain $p2
}
To summarize the operation of the above code, a task is initiated to
compute expr $a*10
. The second piece of computation is chained to it
so that once the first one finishes, the second one is
initiated. Finally, the async_chain
links the promise returned by a
call to the demo
command to the result (promise) of the second
computation. Usage would be as follows:
% set p [demo 4 2]
::oo::Obj73
% $p done puts
42
(A reminder that the done
method is not synchronous either; it
queues the command, puts
in this case, to be invoked when the
promise is fulfilled.)
Although considerably simpler than an equivalent version that did not
use promises, our asynchronous demo
above still needs some effort to
wrap one's head around it. The flow of control via then
and
then_chain
is non-obvious unless you are well-versed with promises.
It would be nice to simplify all that boilerplate. This is where
the await
command comes in.
await
commandThe await
command allows a command created with async
to suspend
(without blocking the application as a whole) until a specified
promise is settled. If the promise is fulfilled, the await
command
returns the value with which the promise was fulfilled. If the promise
is rejected, the await
command raises an error accordingly. Our last
example can then be written as follows:
async demo {a b} {
set 10x [await [ptask "expr $a * 10"]]
set result [await [ptask "expr $10x + $b"]]
return $result
}
Since a command defined via async
always returns a promise even when
the result is explicitly returned as above, usage is same as before.
% set p [demo 4 2]
::oo::Obj73
% $p done puts
42
Notice how much clearer the flow is for this sequence of
asynchronous steps. Moreover, the result is available directly as
the return value from await
making it easier to use as opposed to
the use of then
, done
and friends as is required with direct
use of promises.
NOTE: Keep in mind that the await
command can only be used
within the context of an async
command (including any procedures
called within that context).
A command created via async
always returns a promise and thus all
error handling is done in the same manner as has been described
before using the reject handlers
registered via then
, catch
etc. on the promise returned by the
command. Any errors within the body of the command are automatically
translated to the corresponding promise being rejected.
The await
command, used within a async procedure body, raises
exceptions on error like any other Tcl command and can be handled
using catch
, try
etc. If unhandled, it percolates up and causes
the containing async procedure promise to be rejected.
The promise
package being loosely based on ES7, the following
articles describing the ES7 versions of await
/async
may be useful.