Version 1 of Coroutines for event-based programming

Updated 2008-08-26 20:38:51 by NEM

NEM 2008-08-26: In A brief introduction to closures and continuations for event-based programming I described a simple programming task which is very simple to write when interacting with a console or other synchronous device, but which becomes trickier to implement in an event-oriented manner. The difficulty is that we not only have to change the particular implementation of our software, but we also have to restructure the flow of control of the entire program to accommodate an event-based approach. I then discussed how various programming features, such as closures (which we (can) have), and continuations (which we don't) can help to improve this situation. In this article, I want to show how the experimental coroutine facilities provided in the 8.6 alphas can be used to address the same task. Hopefully this demonstrates a powerful example of what these facilities can achieve, whilst focussing attention on those areas that might need more thought.

As a recap, the original problem is to write a program that asks the user for two numbers and then displays there sum. The "spec" for the program is represented by this simple main routine, written in direct style:

proc main {} {
    set x [ask "First number:"]
    set y [ask "Second number:"]
    tell "Sum = [expr {$x + $y}]"
}

The task then is to implement suitable tell and ask procedures, ideally without having to alter the main routine at all. For a simple console application, this is straight-forward:

proc ask question {
    puts -nonewline "$question "
    flush stdout
    gets stdin
}
proc tell message { puts $message }
main ;# Works as expected

When we move to a Tk GUI implementation, however, things start to fall apart. The trouble is that Tk GUIs just don't work in this direct style; they work in an event-based style. In the above linked page, I show how to adapt the program to this new style, and then how to slowly move back to something representing the simple direct program. I won't recap that here. I did however, hint at how continuations can be used to completely recapture the direct style program. In this page, I want to show how a similar result can be achieved using the coroutines that MS has provided for us. Indeed, the kind of coroutines implemented are roughly equivalent to a kind of one-shot continuation. We still have to make some minor alterations to our main routine, but it should be still pretty recognisable. Here it is:

proc main {k} {
    set x [ask "First number:" $k]
    set y [ask "Second number:" $k]
    tell "Sum = [expr {$x + $y}]"
}

The only difference is the presence of this mysterious k argument. I'll explain what this is for in a moment. Firstly, here are the Tk/coroutine implementations of ask/tell:

namespace path ::tcl::unsupported
proc ask {q callback} {
    toplevel .ask
    pack [label .ask.l -text $q] [entry .ask.e]
    raise .ask; focus .ask.e
    bind .ask.e <Return> [format {
	set ans [.ask.e get]
	destroy .ask
	%s $ans
    } $callback]
    yield
}
proc tell msg { tk_messageBox -message $msg }

These are essentially the same as the callback versions from the previous page, but with a mysterious yield at the end of the ask procedure. To tie this all together, we implement a sort of call-with-current-continuation over the coroutine mechanism:

proc call/cc cmd {
    set k [gensym]
    coroutine $k $cmd $k
}
proc gensym {{prefix "coroutine"}} {
    variable gensymid
    return $prefix[incr gensymid]
}

Finally, we can call our main:

call/cc main

If you run this, you'll see that it does in fact work.

So what's actually going on here? Well, the trick is to make the main routine itself into a co-routine, so that it can be interrupted and resumed as events occur. The call/cc procedure simply creates a unique coroutine and then passes the name of it as an argument to the coroutine procedure itself (the main). This becomes the k argument. The ask procedure then creates the GUI and uses this coroutine argument as the callback to be called when the user enters data. It then calls yield cause the main computation to suspend (and thus yielding back to the event loop). The event GUI then carries on until the user enters a number and hits Return. At this point, the coroutine is invoked, resuming the main computation and returning the result as the result of the yield. This then becomes the result of the ask procedure as if it had worked exactly like our original synchronous version. Voila!

Hopefully this is comprehensible. So, what conclusions/issues can we draw from this?

  • The proposed coroutine mechanism is very powerful, and meshes well with existing Tcl/Tk idioms.
  • In particular, this is a promising approach to taming event-based programming.
  • However, the main procedure still had to be slightly rewritten (it had to be in a proc for one, and needed an extra argument).
  • There is the need to generate unique names for the coroutines (not a massive burden).
  • It would be nice to be able to hide the details entirely within the ask procedure, as would be possible with a real call/cc.

NEM Discussion welcome.