Version 2 of A Coroutine-Enabled Interactive Command Line

Updated 2009-07-31 15:36:54 by CMcC

NEM 2009-07-31: Here's a simple interactive tclsh-style application, with the difference that it runs inside a coroutine. This allows you to have complex event-driven interactions appear as if they are happening in a blocking, non-event-driven manner. For example, you can have Tk GUI applications responding to events while you are apparently in a blocking HTTP request.

# corotcl --
#
#       A coroutine-enabled tclsh
#
package require Tk
proc lambda {params body args} {
    list ::apply [list $params $body] {*}$args
}
proc prompt p {
    puts -nonewline "$p "
    flush stdout
    fileevent stdin readable [lambda return {
        $return [gets stdin]
    } [info coroutine]]
    yield
}
proc get-command {} {
    set cmd [prompt %]
    while {![info complete $cmd]} {
        append cmd \n [prompt >]
    }
    return $cmd
}
proc repl {} {
    while 1 {
        set cmd [get-command]
        set code [catch { uplevel #0 $cmd } result opts]
        if {$code == 1} {
            puts [dict get $opts -errorinfo]
        } else { puts $result }
    }
}
coroutine main repl
#vwait forever ;# if no Tk

An example of use. First, we bring up a simple Tk interface to check that everything keeps running in the background:

% pack [button .b -text Test -command {puts Test}]
.b

Testing this will show that it does print "Test" in the console when clicked. So far, so normal. Now, we create a coroutine-enabled version of http::geturl:

% package require http
2.7.2
% proc fetch url {
>   http::geturl $url -command [info coroutine]
>   yield
> }

We can now use this to fetch data from the web, just like the usual geturl. The difference is that this works in the background: you can still click on the Tk button and it will still print to the console even while the fetch is still in progress:

% set t [fetch http://wiki.tcl.tk/4]
Test
Test
Test
::http::1

There we go: an interactive Tcl interpreter that is fully coroutine enabled.

Next step? Futures?


CMcC - 2009-07-31 11:36:54

Very cool.

Here's a variant which uses a TclOO object called Shell so one can have multiple of these constructed with different chans as i/o.

    # Shell -- a coroutine enabled tcl evaluator
    #
    # From corotcl http://wiki.tcl.tk/24060

    package require TclOO
    namespace import oo::*

    class create Shell {
        constructor {{in stdin} {out ""}} {
            if {$out eq ""} {
                if {$in eq "stdin"} {
                    set out stdout
                } else {
                    set out $in
                }
            }
            chan configure $out -buffering line

            proc prompt {in out p} {
                puts -nonewline $out "$p "
                chan flush $out
                chan event $in readable [list ::apply {{return in} {
                    $return [gets $in]
                }} [info coroutine] $in]
                return [yield]
            }

            proc repl {in out} {
                while {1} {
                    set cmd [prompt $in $out %]
                    while {![info complete $cmd]} {
                        append cmd \n [prompt $in $out >]
                    }

                    switch -- [catch {
                        uplevel #0 $cmd
                    } result eo] {
                        1 {
                            chan configure $out -buffering none
                            puts $out [dict get $eo -errorinfo]
                            chan configure $out -buffering line
                        }
                        2 -
                        3 {
                            break
                        }
                        4 {
                            continue
                        }
                        default {
                            puts $out $result
                        }
                    }
                }
                chan event $in readable {}
                return $result
            }

            coroutine [self]_CORO repl $in $out
        }
    }

    if {[info exists argv0] && ($argv0 eq [info script])} {
        Shell new
        vwait forever
    }