callback

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" ([2 ], [1 ]).

In some cases, as when a -command option is passed to lsort, the receiving command (see below) 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, etc, 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

The receiving command (not a standard term) is the command with which the callback is registered. It usually executes the callback immediately or at a later time (it might in practice arrange for the callback to be executed by some other entity).

When executing a callback 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? ?argument...?. A callback action shouldn't be too complicated to be expressed within a normal command. (In this invocation, "arguments" are those, if any, passed in to further specify the action, distinct from any arguments added by the receiver.)
  • 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> ?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.

Error handling

The raison d′etre for some callback receivers is to introduce a layer of error handling, and the purpose of some callbacks is to prevent or handle errors (e.g. -validatecommand options). The following is about errors in callback scripts.

Handling errors in the callback script itself is a questionable practice: callbacks should have high cohesion (handling errors inside commands called by the callback script is of course still good practice).

Deferred callbacks can lead to errors that may be triggered asynchronously by different kinds of events and don't propagate through the stack: interp bgerror should be your friend.

When writing a receiving command, a basic sanity check on callback scripts can be reasonable, but most generic receiving commands should not presume to assess the wisdom of the callback script. Receivers might still want to be careful with callback scripts, executing them in slave interpreters or by try / catch (an injection attack is basically just a form of callback).

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 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.