invoke

NEM 9 May 2007: Here is a little proc that I use quite regularly. It is useful for avoiding Quoting Hell in the common case of invoking a callback command with some additional arguments. Typically we expect the callback to consist of one or more words given as a list (e.g. "string match"), which is the usual case for built-in commands (e.g. lsort -command, or after). Usually you'd invoke the command using uplevel, but you have to take care to make sure arguments are quoted correctly while still expanding the command, ending up with some code like:

 uplevel #0 $callback [list $arg1] [list $arg2] ..

This works well, but is a bit hard to read if you are not familiar with the idiom. This is where invoke comes in: it handily packages up this familiar idiom into a single command:

 proc invoke {level cmd args} {
     if {[string is integer -strict $level]} { incr level }
     uplevel $level $cmd $args
 }
 invoke #0 $callback $arg1 $arg2 ...

To be a bit more efficient, we can ensure uplevel is passed a single list, which avoids any possibility of string concatenation (Tcl_UplevelObjCmd does its best to avoid this, but cannot always do so):

 proc invoke {level cmd args} {
     if {[string is integer -strict $level]} { incr level }
     # Note 8.5ism
     uplevel $level [linsert $cmd end {*}$args]
 }

I'm not sure if this really makes a noticeable impact on performance in typical cases, though. (If $cmd doesn't have a string rep, then uplevel does this automatically, from my reading of the source code).

Lars H: As I sort-of remarked in "data is code", a built-in alternative to the #0 case

 uplevel #0 $callback [list $arg1] [list $arg2] ..

is

 namespace inscope :: $callback $arg1 $arg2 ..

This is no good for callbacks that expect to access local variables in their calling context, though; the context is still available (which it wouldn't be with [uplevel #0]), but it's at [uplevel 2], not [uplevel 1] as it would be after a normal call.

Also, in 8.5 the plain case

 invoke 0 $callback $arg1 $arg2 ..

is just

 {*}$callback $arg1 $arg2 ..

NEM: All callbacks I know of in Tcl use the uplevel #0 semantics, so any namespace or callframe context is not available. The callback should take care of restoring any such context by for instance using namespace code or providing any variable values it needs as part of the callback (e.g. [list $callback $var1 $var2...]).

Lars H: Most callbacks called from C tend to have eval semantics, e.g. the lsort -command option:

  proc mycompare {args} {
     for {set n 0} {$n + [info level] > 0} {incr n -1} {
        puts "Level $n: [info level $n]"
     }
     eval [list string compare] $args
  }
  proc wrap {script} {eval $script}
  wrap {wrap {lsort -command mycompare {b c a}}}

writes

  Level 0: mycompare b c
  Level -1: wrap {lsort -command mycompare {b c a}}
  Level -2: wrap {wrap {lsort -command mycompare {b c a}}}
  Level 0: mycompare b a
  Level -1: wrap {lsort -command mycompare {b c a}}
  Level -2: wrap {wrap {lsort -command mycompare {b c a}}}

This is sometimes useful, but other times surprising. (I recently tried to understand why

  trace add execution someProc leave {lappend L}

didn't add any data to L although the someProc clearly was being called. It worked much better when I made it {lappend ::L}.)

Promising a caller that a callback will be evaluated in the calling context can get tricky if you need to pass it on to a helper proc though, so aiming for uplevel #0 semantics in code you design yourself is probably a good idea. However, it is sometimes very convenient to go non-functional and communicate via direct access to the context.