This is not necessarily the current version of this TIP.
| TIP: | 379 |
| Title: | Add a Command for Delivering Events Without Tk |
| Version: | $Revision: 1.8 $ |
| Author: | Will Duquette <will at wjduquette dot com> |
| State: | Draft |
| Type: | Project |
| Tcl-Version: | 8.7 |
| Vote: | Pending |
| Created: | Sunday, 17 October 2010 |
| Keywords: | event |
This proposal defines the hook ensemble command, which implements the Subject/Observer pattern. It allows subjects, which may modules, objects, widgets, etc., to synchronously send events to an arbitrary number of subscribers, called observers. A subject may send any number of distinct events, and any number of observers can bind callbacks to a particular event sent by a particular subject. Event bindings can be queried and deleted.
Tcl modules usually send notifications to other modules in two ways: via Tk events, and via callback options like the text widget's -yscrollcommand option. Tk events are available only in Tk, and callback options require tight coupling between the modules sending and receiving the notification.
Loose coupling between sender and receiver is often desirable, however. In Model/View/Controller terms, a View can send a command (stemming from user input) to the Controller, which updates the Model. The Model can then send an event to which all relevant Views subscribe. The Model is decoupled from the Views, and indeed need not know whether any Views actually exist.
At present, Tcl/Tk has no standard mechanism for implementing loose coupling of this kind. This proposal defines a new command, hook, which implements just such a mechanism.
The hook command manages a collection of event bindings. An event binding has four elements:
A subject: the name of the entity that will be sending the event.
The event itself: an occurrence in the life of the subject that other entities might care about. An event has a name, and may also have arguments. By convention, event names are written in mixed-case in angle brackets, e.g., <MyEvent>. Each subject must document the names and arguments of the events it can send.
The name of the observer that wishes to receive the event from the subject.
A command prefix to which the event arguments will be appended.
For convenience, this TIP, collectively refers to subjects and observers as objects, while placing no requirements on how these objects are actually implemented. An object can be a a TclOO or Snit or XOTcl object, a Tcl command, a namespace, a module, a pseudo-object managed by some other object (as tags are managed by the Tk text widget) or simply a well-known name.
Subject and observer names are arbitrary strings; however, as hook might be used at the core and package level, it's necessary to have conventions that avoid name collisions between packages written by different people.
Therefore, any subject or observer name used in core or package level code should look like a Tcl command name, and should be defined in a namespace owned by the package. Consider, for example, an ensemble command ::foo that creates a set of pseudo-objects and uses hook to send notifications. The pseudo-objects have names that are not commands and exist in their own namespace, rather like file handles do. To avoid name collisions with subjects defined by other packages, users of hook, these ::foo handles SHOULD have names like ::foo::1, ::foo::2, and so on.
Because object names are arbitrary strings, application code can use whatever additional conventions are dictated by the needs of the application.
The hook command is an ensemble command with the following subcommands:
This subcommand is used to create, update, delete, and query event bindings.
hook bind ?subject? ?event? ?observer? ?cmdprefix?
Called with no arguments, hook bind returns a list of the subjects with active bindings.
Called with one argument, a subject, hook bind returns a list of the events for which the subject has active bindings.
Called with two arguments, a subject and an event, hook bind returns a list of the observers which are bound to this subject and event.
Called with three arguments, a subject, an event, and an observer, hook bind returns the binding proper, the command prefix to be called when the event is sent, or the empty string if there is no such binding.
Called with four arguments, hook bind creates, updates, or deletes a binding. If binding is the empty string, hook bind deletes any existing binding for the subject, event, and observer. Otherwise, binding must be a command prefix taking as many arguments as are documented for the subject and event.
If the observer is the empty string, "", hook will create a new binding using an automatically generated observer name of the form ::hook::ob<number>. The automatically generated name will be returned, and can be used to query, update, and delete the binding as usual.
The subject and event can contain wildcards; the observer will be bound to all matching subjects and events. The wildcards will be resolved when events are sent.
The cmdprefix may contain %s, %e, and %o conversions, which are expanded at call time to the actual subject, object, and observer names.
Subject/Event Order: It has been suggested that the event name should precede the subject name, as an observer might want to subscribe to the same event from all subjects. However, the subject has priority, as it is the subject that determines the events that are available, and the semantics of those events. A "cannon" object and a "house" object might both have <Fire> events, but it's unlikely that a single callback would reasonably handle both of them.
Optional Observers: It has been suggested that the observer argument should follow the cmdprefix argument; if it is omitted, an observer name would be automatically generated. However, the observer name is frequently used in practice, and is likely to be much shorter than the cmdprefix, which might be quite long. As a general rule, short arguments following long ones tend to get lost visually; keeping the observer before the cmdprefix leads to more easily readable code.
hook send subject event ?args...?
This command is called when the named subject wishes to send the named event. All relevant bindings are called with the specified arguments in the global namespace. Note that the bindings are called synchronously, before hook send returns; this allows the args to include references to entities that will be cleaned up as soon as the event has been sent.
The order in which the bindings are called is undefined. If sequence among observers must be preserved, define one observer and have it call the others in sequence.
Because the hook mechanism is intended to support loose coupling, it is presumed that the subject has no knowledge of the observers, nor any expectation regarding return values. (But see the discussion of the hook call command, below.) This has a number of implications:
hook send returns the empty string.
Normal return values from binding commands are ignored.
Error values are handled by the interp bgerror mechanism by default. Because the subject has no knowledge of the observers, it has no particular competence at handling their errors. That makes it an application issue, and subject to the application policy for handling background errors. Hence, interp bgerror is used.
Observers have no reason to affect the execution of hook. Hence, any other exceptional return codes (e.g., continue or break) will cause hook to throw an error on behalf of the particular binding. The error will be handled as though the binding had thrown it.
If the -errordelete configuration option is true, bindings throwing errors will be deleted so that it doesn't happen again.
If the -bgerror configuration option is false, errors in bindings will propagate to the caller of hook send in the normal way, possibly preventing subsequent bindings from being called.
See hook configure, below, for more information on configuration options.
hook forget object
This command deletes any existing bindings in which the named object appears as either the subject or the observer.
hook cget option
This command returns the value of one of the hook command's configuration options.
hook configure option value ...
This command sets the value of one or more of the hook command's configuration options:
This boolean flag, if true (the default), means that errors in binding scripts are handled by the interpreter's interp bgerror handler. If false, errors in binding scripts are propagated to the caller of hook send.
The boolean flag, if true (the default), means that bindings that throw an error are deleted, so that the error doesn't recur. I can see that this is a useful feature; in at least one of the applications in which I've used this mechanism, a setting of false is genuinely what's wanted. Hence, I've added this option.
The option's value should be a command prefix taking four arguments: a subject, an event, a list of event argument values, and a list of objects receiving the event. The command will be called for each event that is sent. This allows the application to trace event execution for debugging purposes.
The ::model module sends the <Update> event in response to commands that change the model's data:
hook send ::model <Update>
The .view megawidget displays the model state, and needs to know about model updates. Consequently, it subscribes to the ::model's <Update> event.
hook bind ::model <Update> .view [list .view ModelUpdate]
When the ::model sends the event, the .view's ModelUpdate subcommand will be called.
Later the .view megawidget is destroyed. In its destructor, it tells the hook that it no longer exists:
hook forget .view
All bindings involving .view are deleted.
During discussions on the tcl-core mailing list, members suggested a number of possible additions to the functionality described in the first draft of this TIP. Some small capabilities have been added in this draft; however, there are two significant ones that I have elected to defer, for two reasons:
Though reasonable additions, they are somewhat orthogonal to the functionality provided here.
They are somewhat speculative, as they reflect patterns I've not actually used, whereas the functionality described above has been in use in real applications for the past five years.
Consequently, I'd rather not delay this TIP until these suggested additions are mature. If this TIP is accepted, then these additions can be considered as TIPs in their own right.
On the other hand, they are genuinely interesting, so I want to mention them here.
The hook send command calls bindings synchronously, returning after all bindings have been called. An asychronous mode has been proposed, where bindings would be called in the context of the event loop for even looser coupling between the subject and the observers. This is certainly doable; however, the same effect can be achieved by calling hook send in an after handler.
I've often considered adding a mode like this to our existing implementation, but have always thought better of it in the end.
Two members of tcl-core have suggested that a hook call command would be useful to support plug-in architectures. hook call is like hook send, except that it would capture the return values of all observer bindings for use by the caller. This is an interesting notion; but I'm not sure how to get it right. In some use cases, it would be enough just to get a list of the return values. In other uses cases, the caller might want to know the observer, the return value, and the complete dictionary of return options.
This command could easily use the same table of bindings as hook send, but it wouldn't change the basic infrastructure particularly. Consequently, it can be easily added later, should this TIP be accepted.
A prototype implementation is available at [1]. It is written in Tcl, and makes use of SQLite3. Provided that SQLite3 is available, the prototype implementation should work in both Tcl 8.5 and Tcl 8.6.
Note: The prototype is still for the old notifier API. I'll update it after the community has had a chance at this draft of the TIP.
This document has been placed in the public domain.
This is not necessarily the current version of this TIP.