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. If you construct it with a 'port' argument, it'll listen on localhost.
# Shell -- a coroutine enabled tcl evaluator # # From Neil Madden's corotcl http://wiki.tcl.tk/24060 # # Usage: # # Shell new - create a shell listening on stdio # Shell new in $chan out $chan - shell connected to chan # Shell new port $port - shell server on localhost port $port
package require TclOO namespace import oo::*
package provide Shell
class create Shell {
variable interp constructor {args} { # prompt for input, collect it and return 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] } # read-eval-print loop - prompt, gets input, evaluate it, print result proc repl {self in out} { variable interp while {1} { set cmd [prompt $in $out %] while {![info complete $cmd]} { append cmd \n [prompt $in $out >] } try { {*}$interp $cmd } on error {result eo} { puts $out [dict get $eo -errorinfo] } on return {result} { break } on ok {result} { puts $out $result } } # close the i/o unless it's stdio if {$in ne "stdin"} { chan close $in read } else { chan event $in readable {} ;# stop listening to stdin } if {![string match std* $out]} { chan close $in write } return $result } set interp {uplevel #0} set in stdin; set out "";# default - use stdio set host localhost ;# default - listen only to localhost dict with args { if {[info exists port]} { # what is wanted is a listener socket -server [list ::apply {{sock addr port} { set shell [Shell new in $sock] }}] -myaddr $host $port } else { # we have a chan (or a couple of chans) if {$out eq ""} { if {$in eq "stdin"} { set out stdout } else { set out $in } } chan configure $out -buffering line coroutine [self]_CORO repl [self] $in $out } } }
}
if {info exists argv0 && ($argv0 eq info script)} {
puts "Shell on stdio" Shell new puts "Shell on localhost port 8082" Shell new port 8082 interp {uplevel #1} vwait forever
}