Version 3 of callback

Updated 2017-11-10 12:19:55 by foo

What is a callback?

A callback is "executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at a given time" ([L1 ], [L2 ]).

In some cases, as when a -command option is passed to lsort, the receiving command immediately executes the code (blocking, or synchronous, callback). More commonly, a callback is specified in an after, bind, trace, I/O handler, or widget -command option and called when appropriate (event-oriented, deferred, or asynchronous, callback). A deferred callback must be able to execute even if the context where it was defined has since been destroyed.

Defining / expressing callbacks

In Tcl, callbacks are typically either expressions, lambda expressions, or scripts. An expression is evaluated using expr and a lambda expression by apply. They will not be discussed further here.

Callback scripts are lists of words (with wrinkles such as when one prepends a + character to a bind callback, which is part of the callback but not of the script proper). The script can be a command prefix, i.e. a script that expects that further arguments will be concatenated to it at the point of execution, at the receiving command's discretion.

Execution

When executing a passed script, the executing code will typically use something like eval or uplevel (or other evaluating command), or a command substitution ({*}$script). It adds arguments of its own if the script is used as a command prefix, using some variation of, e.g.

eval $script [list $a $b]
if <condition> [concat $script [list $a $b]]
[{*}$script $a $b]

Name resolution

As the script is executed, any command and variable names in the script that aren't namespace qualified already will get resolved to the best of the interpreter's ability. Name resolution is in itself a predictable procedure, but it is affected by dynamic conditions that might be unknown when writing the program. This makes passing a script as an argument and executing it either wonderfully versatile or frighteningly ambiguous, depending on one's point of view and how clever one tries to be.

Good practices for callback scripts

  • Consider writing a command, and using the command invocation as the complete content of the script, i.e list <command name> ?subcommand? ?pre-argument...?. A callback action shouldn't be to complicated to be expressed within a normal command. (A "pre-argument" is meant to signify a parameter passed in to further specify the action beyond what the receiver-added arguments do.)
  • If the callback is too complicated for a command, consider writing the script as the invocation of a TclOO method, i.e. list <instance or object> <method> ?pre-argument...?.
  • For callback scripts that depend on the namespace context of their sender, use namespace code. A script wrapped in such an invocation will be executed in the current namespace regardless of which namespace the receiver uses.
  • Use fully qualified names unless there is a good reason to let the name resolution procedure in the interpreter figure out the name.

Customization tricks

Spoofing a namespace capture
namespace code generates a script that calls namespace inscope on the namespace name and the original script. If you pass the result of list namespace inscope <some namespace> <script>, the receiver is none the wiser.
Setting namespace dynamically
use [namespace current]::<name> for selected names in the script when setting the callback in a dynamic context. Remember to let the interpreter evaluate the script as an argument (i.e. no braces!), otherwise the namespace current will instead be run, reduntantly, at the point where the script is executed.
Forcing namespace
to force a namespace context at the point where the script is executed, execute a passed script $script using namespace eval <namespace> $script. Only works with unqualified names and scripts not wrapped in namespace inscope.
Passing a namespace
pass a bundle in the form of a list consisting of a namespace name and a script, execute as namespace eval {*}$bundle. Similar to "spoofing", above, except that it can't be executed by any other execution command.
Querying namespace
at either the point of passing the script or the point of execution, namespace which can be used to find out what qualified name the name resolution procedure results in for a given name.
Faking inheritance
the parent namespace usually isn't searched during name resolution. You can change that with namespace path:
namespace path [linsert [namespace path] 0 [namespace parent]]
Seeding the resolution
...the previous point being a special case of using namespace path to insert arbitrary namespaces into the resolution process between the current and the global namespace.

Why do global names dominate callback scripts in the Tcl code corpus?

The grandfathered protocol for callbacks is to use only global names, because that was what was originally the only option. This continues to work both in pre-namespace versions of Tcl and in modern ones, with the caveat (or opportunity) that later, namespace-aware, versions may resolve a name to an unexpected namespace since the global namespace is the last one to be considered during the resolution procedure.