package require snit 0.7 ::snit::type name definition ::snit::widget name definition
In developing Snit I had the following goals:
Using Snit, a programmer can:
% package require snit 0.7 % ::snit::type stack { variable data method push {value} { lappend data $value return } method peek {} { lindex $data end } method pop {} { set value [lindex $data end] set data [lrange $data 0 end-1] return $value } } ::stack % stack mystack ::mystack % mystack push a % mystack push b % mystack peek b % mystack pop b % mystack peek a %Here's another example: a read-only text widget. Everyone knows how to make a text widget read-only by disabling it, but that way is fraught with annoyance. Key bindings don't work well, and selections are invisible on some platforms. Here's a better way to do it:
package require Tk package require snit ::snit::widget rotext { constructor {args} { # Create the text widget; turn off its insert cursor component hull is [text $self -insertwidth 0] # Apply any options passed at creation time. $self configurelist $args } # Disable the text widget's insert and delete methods, to # make this readonly. method insert {args} {} method delete {args} {} # Enable ins and del as synonyms, so the program can insert and # delete. delegate method ins to hull as insert delegate method del to hull as delete # Pass all other methods and options to the real text widget, so # that the remaining behavior is as expected. delegate method * to hull delegate option * to hull }You've now got a "rotext" megawidget. Use the "ins" and "del" widget subcommands to insert and delete text; they are just like the standard text widget's "insert" and "delete" commands. In every other way, rotext acts just like a text widget because it delegates everything to a text widget.
In fact, Snit lets you delegate methods to anything that looks at all like an object and options to any object that supports "configure" and "cget". Instances of the following type are drop-in replacements for the standard "string" command:
::snit::type stringfactory { constructor {args} { component stringhandler is string } delegate method * to stringhandler }The "component" command in the constructor associated the logical name "stringhandler" with a specific object; the "string" command, in this case. The "delegate" command delegates all unknown methods (which is all methods, since stringfactory defines none of its own) to the "stringhandler" component. Thus, any instance of type stringfactory will behave precisely like the "string" command.
The best example of abstract data types in Tcl/Tk are the Tk widgets. Whereas most GUI toolkits provided a complicated inheritance hierarchy of components, Tk simply provides a non-hierarchical set of powerful abstract GUI types--the Tk widgets. They have a deceptively simple interface. Snit adopts the same conventions.
In Tk, a type is a Tcl command that creates instances--objects--which belong to the type. Most types define some number of Options which can be set at creation time, and usually can be changed later.
Further, an instance is also a Tcl command--a command that gives access to the operations which are defined for that abstract data type. Conventionally, the operations are defined as subcommands, or methods of the instance command. For example, to insert text into a Tk text widget, you use the text widget's "insert" method:
# Create a text widget and insert some text in it. text .mytext -width 80 -height 24 .mytext insert end "Howdy!"In this example, "text" is the type command and ".mytext" is the instance command.
Snit expands upon this model in several ways:
% snit::type dog { option -breed mongrel option -color brown option -akc 0 option -shots 0 } ::dogAccording to this, a dog has four notable properties, or options: a breed, a color, a flag that says whether it's pedigreed with the American Kennel Club, and another flag that says whether it has had its shots. The default dog, evidently, is a brown mutt.
The ::dog command can now be used to define individual dogs. Any or all of the options may be set at creation time.
% dog spot -breed beagle -color "mottled" -akc 1 -shots 1 ::spot % dog fido -shots 1 ::fidoSo ::spot is a pedigreed beagle; ::fido is a typical mutt, but his owners evidently take care of him, because he's had his shots.
Options can be retrieved using the cget
instance method:
% spot cget -color mottled % fido cget -breed mongrelAny number of options may be set at one time using the
configure
instance method. Suppose that closer inspection
shows that ::fido is a rare Arctic Boar Hound of a lovely dun color:
% fido configure -color dun -breed "Arctic Boar Hound" % fido cget -color dun % fido cget -breed Arctic Boar Hound
% snit::type dog { option -breed mongrel option -color brown option -akc 0 option -shots 0 method bark {} { return "$type $self barks" } method chase {thing} { return "$type $self chases $thing" } method register {} { $self configure -akc 1 } } ::dog % dog spot -breed labrador -color chocolate ::spot % spot bark ::dog ::spot barks % spot chase cat ::dog ::spot chases cat % spot cget -akc 0 % spot register % spot cget -akc 1Instance methods are like Tcl procs, but they are called as subcommands of the object's instance command. There are several things to note about them:
% snit::type dog { variable barkCount 0 method bark {} { incr barkCount if {$barkCount % 3 == 0} { return "$type $self HOWLS!" } else { return "$type $self barks." } } } ::dog % dog spot ::spot % spot bark ::dog ::spot barks. % spot bark ::dog ::spot barks. % spot bark ::dog ::spot HOWLS! % spot bark ::dog ::spot barks.As you see, instance variables defined explicitly in the type definition are automatically visible in instance methods; there's no need to declare them.
Note that instance variables can be arrays; however, array instance
values cannot be initialized as part of the variable definition.
Initialize them in the type's constructor
instead.
It's also possible to define instance variables implicitly by declaring them in instance method bodies using the "variable" command, much as one could do in normal Tcl code; however, this form is supported mostly for compatibility with older versions of Snit. It's usually best to define all instance variables explicitly in the type definition.
% snit::type dog { typevariable howlCount 0 variable barkCount 0 method bark {} { incr barkCount if {$barkCount % 3 == 0 || $howlCount > 0} { incr howlCount return "$type $self HOWLS!" } else { return "$type $self barks." } } } ::dog % dog spot ::spot % dog fido ::fido % spot bark ::dog ::spot barks. % fido bark ::dog ::fido barks. % spot bark ::dog ::spot barks. % spot bark ::dog ::spot HOWLS! % fido bark ::dog ::fido HOWLS! % spot bark ::dog ::spot HOWLS!As with instance variables, type variables defined explicitly in the type definition are also automatically visible in all instance methods. And also as with instance variables, type variables can be defined implicitly by declaring them in a method using the "typevariable" command. Again, this is made possible mostly to support earlier versions of Snit; it's usually best to define type variables explicitly.
create
, which is assumed implicitly if the first
argument to the type command isn't a type method name:
% # Either of these works % dog spot ::spot % dog create fido ::fidoYou can define your own typemethods; for example, suppose we want to stop the dogs from howling:
% snit::type dog { typevariable howlCount 0 variable barkCount 0 method bark {} { incr barkCount if {$barkCount % 3 == 0 || $howlCount > 0} { incr howlCount return "$type $self HOWLS!" } else { return "$type $self barks." } } typemethod quiet {} { set howlCount 0 return "type $type is now quiet" } } ::dog % dog spot ::spot % dog fido ::fido % spot bark ::dog ::spot barks. % fido bark ::dog ::fido barks. % spot bark ::dog ::spot barks. % spot bark ::dog ::spot HOWLS! % fido bark ::dog ::fido HOWLS! % spot bark ::dog ::spot HOWLS! % dog quiet type ::dog is now quiet % spot bark ::dog ::spot barks.As you can see, the variable "type" is automatically defined in all type methods, as are all type variables.
If an instance method needs to call a type method, it does so using its "type" variable. For example, suppose a method needs to call a type method to clear a type variable:
% snit::type mycounter { typevariable counter 15 typemethod clear {} { set counter 0 } method clear {} { $type clear } # Plus some other code to make this type do something interesting. } ::mycounter % mycounter mc ::mc % # These are equivalent, now % mycounter clear 0 % mc clear 0Note also that type and instance method names can overlap; there's no problem having a type method called "clear" and an instance method called "clear".
constructor
. The constructor
receives the list of options passed to the type command's create
method and can then do whatever it likes. That might include
computing instance variable values, reading data from files, creating
other objects, updating type variables, and so forth.Suppose that it's desired to keep a list of all pedigreed dogs. The list can be maintained in a type variable and retrieved by a type method. Whenever a dog is created, it can add itself to the list--provided that it's registered with the American Kennel Club.
% snit::type dog { option -akc 0 typevariable akcDogs {} constructor {args} { $self configurelist $args if {$options(-akc)} { lappend akcDogs $self } } typemethod getakc {} { return $akcDogs } } ::dog % dog spot -akc 1 ::spot % dog fido ::fido % dog getakc ::spotThe only difficult thing about this example is the line
$self configurelist $argsThe argument "args" gets the options passed in when the object is created (the "-akc 1" in "dog spot -akc 1"), if any. The "configurelist" method is used to save the option values. Why doesn't Snit do this automatically? There are a variety of reasons, which will be discussed presently; the simplest is that this way the type's creation syntax can be modified, simply by replacing "args" with some other list of argument names.
That is, the constructor's argument list is defined just like that for any Tcl proc. It can include named arguments and default values, and can end with "args" to pick up any remaining arguments on the command line. The arguments can be called anything but "type" or "self", both of which are automatically defined in the constructor's body, just as they are in instance methods.
For standard Tk widget behavior, and to achieve consistency with earlier versions of Snit, use "args" as the sole argument.
% snit::type dog { option -akc 0 typevariable akcDogs {} constructor {args} { $self configurelist $args if {$options(-akc)} { lappend akcDogs $self } } destructor { set pos [lsearch -exact $akcDogs $self] if {$pos != -1} { set akcDogs [lreplace $akcDogs $pos $pos] } } typemethod getakc {} { return $akcDogs } } ::dog % dog spot -akc 1 ::spot % dog fido -akc 1 ::fido % dog getakc ::spot ::fido % spot destroy 0 % dog getakc ::fidoDestructors don't take any arguments; the variables "type" and "self" are defined as usual in the destructor's body.
Instances of a normal snit::type are destroyed as shown, by calling their "destroy" method. snit::widgets are destroyed by the Tk "destroy" command, just like normal widgets.
% snit::type dog { typevariable akcDogs {} option -akc 0 onconfigure -akc {value} { set options(-akc) $value set pos [lsearch -exact $akcDogs $self] if {$value} { if {$pos == -1} { lappend akcDogs $self } } else { if {$pos != -1} { set akcDogs [lreplace $akcDogs $pos $pos] } } } destructor { $self configure -akc 0 } typemethod getakc {} { return $akcDogs } } ::dog % dog spot -akc 1 ::spot % dog fido ::fido % dog getakc ::spot % fido configure -akc 1 % dog getakc ::spot ::fido % fido configure -akc 0 % dog getakc ::spot % spot destroy 0 % dog getakcIn the above listing, we've defined an "onconfigure" handler for the "-akc" option. Whenever the value of the "-akc" flag is set, the new value is passed to the handler ("type" and "self" are defined automatically, as are all instance variables). The handler can do anything, including throwing an error if the value is invalid; normally it should save the value in the "options" array, as shown. Then it updates the "akcDogs" array, adding or deleting the dog's name as necessary.
Note that we've gotten rid of the constructor; if "-akc" is included when the dog is created, the onconfigure will be called automatically. The destructor simply sets the "-akc" flag to 0; this will cause the dog's name to be deleted from the list.
It's also possible to write "oncget" handlers, that are called whenever an option's value is retrieved. The default oncget handler, if written explicitly, would look like this:
oncget -akc { return $options(-akc) }But again, it can be changed to do anything that's desired.
Normally, instance methods will access the "options" array directly; if your code makes use of onconfigure and oncget, though, it's best to use "configure" and "cget" even in instance methods.
% snit::type animal { method eat {what} { return "$type $self eats $what." } } ::animal % snit::type dog { variable animal constructor {args} { $self configurelist $args set animal [animal dog%AUTO%] } method eat {what} { $animal eat $what } } ::dog % dog spot ::spot % spot eat dogfood ::animal ::dog::doganimal1 eats dogfood.In this code, each dog instance delegates its "eat" method to its animal object, which we call a "component" of the dog instance.
There's nothing wrong with this code, but it's not as helpful as it could be; as we'll see in the following sections, there are things Snit could do for us if it knew the names of the components the dog created. So here's a better way to create the animal component:
% snit::type dog { constructor {args} { $self configurelist $args # Note: The animal type is as defined above. component animal is [animal dog%AUTO%] } method eat {what} { [component animal] eat $what } } ::dog % dog spot ::spot % spot eat dogfood ::animal ::dog::doganimal2 eats dogfood.A type can define any number of components.
% snit::type dog { constructor {args} { $self configurelist $args # Note: The animal type is as defined above. component animal is [animal dog%AUTO%] } delegate method eat to animal } ::dog % dog spot ::spot % spot eat dogfood ::animal ::dog::doganimal3 eats dogfood.Now we've defined the dog's "eat" method in just one line of code. There are a number of variations. Suppose, for example, that we were declaring a "wolf" type, the difference being that while dogs eat, wolves devour. But we want to use our underlying animal component anyway. We could write a "devour" method that calls the animal component's eat method, or we could do this:
% snit::type wolf { constructor {args} { $self configurelist $args # Note: The animal type is as defined above. component animal is [animal wolf%AUTO%] } delegate method devour to animal as eat } ::wolf % wolf fenris ::fenris % fenris devour mice ::animal ::wolf::wolfanimal4 eats mice.As you can see, you can not only delegate a method to a component; you can even specify which of the component's methods should be called.
Finally, it's likely that dogs have more in common with animals in general than the fact that they eat. Probably, you want your dogs to exhibit as much normal animal behavior as possible, while adding the dog-specific bits--dogs bark, for example. Do this:
% snit::type dog { constructor {args} { $self configurelist $args # Note: The animal type is as defined above. component animal is [animal dog%AUTO%] } delegate method * to animal method bark {} { return "$self barks." } } ::dog % dog spot ::spot % spot eat dogfood ::animal ::dog::doganimal5 eats dogfood. % spot bark ::spot barks.Now any unrecognized method received by your dogs will be delegated automatically to their animal components. Your dog is essentially an animal, except that it also knows how to bark.
% snit::type animal { option -color } ::animal % snit::type dog { option -akc 0 option -color brown onconfigure -color {value} { [component animal] configure -color $value } oncget -color { [component animal] cget -color } constructor {args} { # Define the animal with the default color component animal is [animal dog%AUTO% -color $options(-color)] # Save any creation options. $self configurelist $args } } ::dog % dog fido -color black ::fido % fido cget -color blackHere, we've written option handlers to pass the "-color" option's value down to the animal component on configure and retrieve it on cget. Note that this is a case where the order in which options are configured in the constructor matters; if "$self configurelist $args" had been called prior to the creation of the animal component, an error would have been thrown. But, as before, there's an easier way to do it.
% snit::type dog { option -akc 0 constructor {args} { # Type animal is as declared above. # Define the animal with the default color component animal is [animal dog%AUTO% -color brown] # Save any creation options. $self configurelist $args } delegate option -color to animal } ::dog % dog spot ::spot % spot cget -color brown % dog fido -color black ::fido % fido cget -color blackAs with delegated methods, you can delegate to an option with a different name; if the animal type had been written by someone in England, for example, it might have a "-colour" instead of a "-color". The delegate statement would look like this:
delegate option -color to animal -colourAnd, as with methods, unrecognized options can be delegated as well:
delegate option * to animal
To make all of this work out properly, every snit::widget must do the following:
For example, suppose that you want to create a specialized label widget:
snit::widget mylabel { constructor {args} { component hull is [label $self] # Customize label as desired $self configurelist $args } delegate method * to hull delegate option * to hull }When a megawidget of this type is first created, Snit defines the object's instance command as usual. You can call methods and configure options normally, right from the beginning of the constructor. But until you've created a real Tk widget called "$self" and saved as the "hull" component, the object isn't really a widget; it's just a normal Snit object with a name that looks like a widget name. When you create a real Tk widget called "$self", Tk also creates the GUI window called "$self" and a Tcl command with the same name that controls that GUI window. Now we have a widget--but "$self" is now the Tk widget's widget command, rather than Snit's instance command. When we save "$self" as the "hull" component, Snit saves the Tk widget's command with a new name, and reinstalls its own instance command as "$self". Now we have a single name that represents both a command that accepts our instance methods, and also a Tk GUI window. Finally, when we delegate any desired methods and options back to the "hull" component, we restore the Tcl interface to that Tk GUI window. We now have an instance command that implements the Tk widget's interface with our own additions.
So what the previous code does is define a snit::widget type called "mylabel" which is for all intents and purposes the same as a standard label.
Above, it says that the hull must be a real Tk widget. In fact, the hull can also itself be a megawidget--but ultimately there has to be a real Tk widget underneath.
The simplest (but not the best) way is to modify the object creation syntax. Normally it looks like this: the type command, followed by the instance name, followed by zero or more options and their values. Any arguments following the instance name are included in the "args" passed to the constructor; the constructor can interpret them any way it pleases. So you could create a "scroller" to scroll a text widget like this:
scroller .scroller text -width 80 -height 40That's not the best way, because it's a change to the standard widget creation syntax. The better way is to define a creation-time option to specify the widget type, and then code it like this:
snit::widget scroller { # Scroll text widgets by default option -thing text # -thing can be retrieved, but can only be set at creation. onconfigure -thing {value} { error "-thing cannot be modified after creation time." } constructor {args} { # FIRST, find out what kind of thing we're scrolling: set options(-thing) [from args -thing] # NEXT, create a frame to contain the thing and the scrollbars component hull is [frame $self] # NEXT, create the thing. component thing is [$options(-thing) $self.thing] # It's now safe to configure the rest of the options $self configurelist $args # NEXT, create the scrollbars, grid everything in, etc. #... } delegate method * to thing delegate option * to thing }The
from
command extracts the named option from the variable
"args"; if -thing doesn't appear in "args", then from
retrieves the default value (or, optionally, a default value can be
included as the third argument). Then, the constructor saves the
value directly into the options array, and proceeds to use it. Now,
we can't change the type of the thing we're scrolling after the
megawidget is created, so we define an onconfigure handler for -thing,
and generate an error whenever -thing is configured. We can do this
safely because we removed -thing from the argument list before we
passed it to "$self configurelist". Since we left the default oncget
handler in please, -thing can be retrieved normally.
For example, suppose that a snit::widget includes, as one component, a label that should display one of the snit::widget's instance variables. One does this by setting the label widget's -textvariable option to the full name of the instance variable. The following, for example, is a mistake:
snit::widget mywidget { variable myvalue 5 constructor {args} { # ... label $self.lab -textvariable myvalue # ... } }The above code causes the label to display the value of a global variable called "myvalue". Instead, use the
varname
command
to return the instance variable's full name:
snit::widget mywidget { variable myvalue 5 constructor {args} { # ... label $self.lab -textvariable [varname myvalue] # ... } }Similarly, the
typevarname
command can be used to return the
full name of type variables.Now, suppose you want your megawidget to include a Tk button, and you need to define the Tcl command the button calls when pushed. There are three possibilities:
For example,
snit::widget mypanel { constructor {args} { # ... button $self.btn1 -text "Instance Method" \ -command [list $self mymethod] button $self.btn2 -text "Type Method" \ -command [list $type mytypemethod] button $self.btn3 -text "Proc" \ -command [list [codename MyProc] $self] # ... } method mymethod {} { # Pushed "Instance Method" } typemethod mytypemethod {} { # Pushed "Type Method" } proc MyProc {self} { # Pushed "Proc" } }The buttons that use instance and type methods are straightforward: because the "type" and "self" command names are always either global or fully-qualified, they can be passed safely to any object as callback commands. Procs are different; procs need to be explicitly qualified using the
codename
command. A couple of things to
note:
snit::type name definition
snit::widget name definition
The type name is then a command which is used to (among other things) create objects of the new type.
The snit::type and snit::widget definition blocks are identical, and may contain the following definitions:
typevariable name ?value?
typemethod name arglist body
Type variables defined in the type definition are automatically visible in the body of every type method.
option name ?defaultValue?
configure
and cget
instance
methods.An option defined in this way is said to be "locally defined".
variable name ?value?
method name arglist body
An instance method defined in this way is said to be "locally defined".
Type and instance variables defined in the type definition are automatically visible in all instance methods. If the type has locally defined options, the "options" array is also visible.
constructor arglist body
The body is a script of commands to be executed. The variables "type" and "self" are automatically defined in the body to be the type's and the instance's fully-qualified names; the arglist may not contain the argument names "type" or "self".
If the constructor is not defined, it defaults to this:
constructor {args} { $self configurelist $args }
For standard Tk widget behavior (or to achieve the behavior of previous versions of snit) the argument list should be the single name "args", as shown.
The constructor's body usually creates component objects; for snit::widget types, it must define the "hull" component.
destructor body
onconfigure name arglist body
If no explicit onconfigure handler is defined for an option, the handler is defined as follows:
onconfigure name {value} { set options(name) $value }If an explicit onconfigure handler is defined, the options array will be updated with the new value only if the handler so updates it.
oncget name body
cget
instance method.If no explicit oncget handler is defined for an option, the handler is defined as follows:
oncget name { return $options(name) }
proc name args body
delegate method name to comp ?as compmethod compargs...?
method name {args...} { [component comp] mymethod args... }If desired, the delegated method may target a method with a different name by using the "as" clause; it may also add arguments to the beginning of the argument list. In that case, it's as though the delegated method were defined as follows:
method name {args...} { [component comp] compmethod \ compargs... args... }If the specified method name is "*", then all unknown method names passed to the instance will be passed along to the specified component. In this case, the "as" clause is not allowed.
A method cannot be both locally defined and delegated.
delegate option name to comp ?as compoption?
configure
, configurelist
, or
cget
instance method is used to set or
retrieve the option's value, the equivalent configure or
cget command will be applied to the component as though
these onconfigure
and oncget
handlers were
defined:
onconfigure name {value} { [component $comp] configure compoption $value } oncget name { return [[component $comp] cget compoption] }If the "as" clause is omitted, the compoption name is the same as name.
Warning: options can only be delegated to a component if it supports the "configure" and "cget" instance methods.
$type typemethod args....
$type create name ?option value ...?
For snit::types, if name is not a fully-qualified command name, it is assumed to be a name in the namespace in which the call to snit::type appears. The method returns the fully-qualified instance name.
For snit::widgets, name must be a valid widget name; the method returns the widget name.
So long as name does not conflict with any defined type method name, the "create" keyword may be omitted.
If the name includes the string "%AUTO%", it will be replaced with the string "$type$counter" where "$type" is the type name and "$counter" is a counter that increments each time "%AUTO%" is used for this type.
$type info typevars
$type info instances
$type destroy
create
type method creates
objects of the type; each object has a unique name which is also a
Tcl command. This command is used to access the object's methods and
data, and has this form:
$object method args...
$object configure ?option? ?value? ...
Two warnings. First, unlike Tk widget options, locally-defined snit::type and snit::widget options do not have a "dbname" or "classname"; Snit never queries the Tk option database. These fields in the returned information will be set to the empty string, {}. Second, the information will be available for delegated options only if the component to which they are delegated has a "configure" method that returns this same kind of information.
$object configurelist optionlist
configure
, but takes one argument, a list of options and
their values. It's mostly useful in the type constructor, but
can be used anywhere.
$object cget option
$object destroy
destructor
and
freeing all related memory.
Note: The "destroy" method isn't defined for
snit::widget
objects; an instance of a snit::widget
is destroyed by calling the Tk "destroy" command, just as
a normal widget is.
$object info type
$object info vars
$object info typevars
$object info options
Note that the return value might be different for different instances of the same type, if component object types can vary from one instance to another.
varname name
typevarname name
codename name
component name ?is obj
Used without the "is" clause, it simply returns the object command associated with the component name. It's an error if no object command is associated with the name.
from argvName option ?defvalue?
from
command plucks an option value from a list of
list of options and their values, such as is passed into a
type's constructor
. argvName must be the name
of a variable containing such a list; option is the name
of the specific option.
from
looks for option in the option list. If
it is found, it and its value are removed from the list, and
the value is returned. If option doesn't appear in the
list, then the defvalue is returned. If the option is a
normal (undelegated) option, and defvalue is not
specified, then the option's default value as specified in the
type definition will be returned instead.
variable name
variable
command, if
desired; or, instance code can use the variable
command to declare instance variables that don't appear in the
type definition.It's generally best to define all instance variables in the type definition, and omit declaring them in methods and so forth.
Note that this is not the same as the standard Tcl "::variable" command.
typevariable name
typevariable
to declare type variables explicitly, if
desired; or, they can use typevariable
to declare
type variables that don't appear in the type definition.It's generally best to declare all type variables in the type definition, and omit declaring them in methods, type methods, and so forth.
So that was one thing--tedium is a powerful motivator. But the other thing I noticed is that I wasn't using inheritance at all, and I wasn't missing it. Instead, I was using delegation: objects that created other objects and delegated methods to them.
And I said to myself, "This is getting tedious...there has got to be a better way." And one afternoon, on a whim, I started working on Snit, an object system that works the way Tcl works. Snit doesn't support inheritance, but it's great at delegation, and it makes creating megawidgets easy.
I should add, I'm not particularly down on Incr Tcl. But "Snit's Not Incr Tcl" occurred to me while I was casting about for a name, and I guess there was a certainly inevitability about it.
If you have any comments or suggestions (or bug reports!) don't hesitate to send me e-mail at will@wjduquette.com. In addition, there's now a Snit mailing list; you can find out more about it at the Snit home page, http://www.wjduquette.com/snit.
Copyright © 2002, by William H. Duquette. All rights reserved.