Snit -- Snit's Not Incr Tcl

SYNOPSIS

    package require snit 0.7
    ::snit::type name definition
    ::snit::widget name definition

OVERVIEW

This man page has three main parts. This section provides a top-level overview of the Snit object framework. The CONCEPTS section describes each feature of Snit in detail, with examples. First time users of Snit should read both this section and the CONCEPTS section carefully. The REFERENCE section documents the syntax of each Snit command. Snit is yet another pure Tcl object and megawidget system. It's unique among Tcl object systems (so far as I know) in that it's a system based not on inheritance but on delegation. Object systems based on inheritance only allow you to inherit from classes defined using the same system, and that's a shame. In Tcl, an object is anything that acts like an object; it shouldn't matter how the object was implemented. I designed Snit to help me build applications out of the materials at hand; thus, Snit is designed to be able to incorporate and build on any object, whether it's a hand-coded object, a Tk widget, an Incr Tcl object, a BWidget or almost anything else.

In developing Snit I had the following goals:

Using Snit, a programmer can:

Consider the following two examples, a stack and a read-only text widget.

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

CONCEPTS

This section describes the important concepts behind the Snit object framework, and how to make use of them. The examples will often use syntax which haven't yet been explained; if they are not clear in context, the REFERENCE section gives a complete (if terse) description of all Snit syntax.

Abstract Data Types

In computer science terms, an abstract data type is a complex data structure along with a set of operations, like a stack, a queue, or a binary tree--that is to say, in modern terms, an object. The term object is usually applied to systems that include some form of inheritance; as Snit doesn't do inheritance, the older term seems more appropriate. Snit is all about creating abstract data types and assembling them into applications.

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:

Options

A type's options are the equivalent of what other object-oriented languages would call public member variables or properties: they are data values which can be retrieved and (usually) set by the clients of an object. If a type is to be used a record type, it's possible that options are all that's needed. Consider the following type, to be used in an application that manages a list of dogs for a pet store:

% snit::type dog {
    option -breed mongrel
    option -color brown
    option -akc 0
    option -shots 0
}
::dog
According 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
::fido
So ::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
mongrel
Any 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

Instance Methods

So far we've seen how to define, set and retrieve properties of our objects using options. It's time to add some behavior.

% 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
1
Instance 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 doesn't have any built-in notion of public and private methods. As a matter of convention, following the standard Tcl style for normal proc names, public method names begin with a lower case character, and private method names begin with an upper case character.

Instance Variables

Often you'll want to store private data with an object--data that's not accessible via an option. You do this with instance variables, which can be defined explicitly or implicitly. For example, suppose every third time Spot barks, he just has to howl instead:

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

Type Variables

Sometimes you'll want to save some data that relates to all instances of the type, and that is accessible from all instances of the type. Such a variable is called a "type variable". For example, once one dog howls all of the nearby dogs start to howl as well:

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

Type Methods

Just as an instance of a type is an object, so also is the type itself an object--it's an object that knows how to create instances. And as an object, it can also have methods. The most important of these type methods is 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
::fido
You 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
0
Note 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".

Constructors

Often it's necessary to do some work to initialize a new object properly; simply setting its options isn't enough. Consequently, Snit allows you to define a 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
::spot
The only difficult thing about this example is the line

        $self configurelist $args
The 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.

Destructors

The example in the previous section showed a constructor which added all pedigreed dogs to a list; it contained a basic error, which is that objects can be destroyed. When an object representing an AKC dog is destroyed, the list of pedigreed dogs needs to be updated. This is done using a destructor:

% 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
::fido
Destructors 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.

Option Handlers

By now you might be asking, "What if they set that -akc flag after the object is created? Suppose the dog is registered with the American Kennel Club later on?" In short, if the "-akc" flag's value is changed during the lifetime of the dog, we should update the "akcDogs" variable accordingly. Here's one way to do it:

% 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 getakc
In 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.

Components

Sometimes an object is complete in itself; but fairly often, especially in GUI applications, an object will create one or more other objects to do part of its job. Suppose, for example, we wanted dogs to behave like other animals; and suppose we had an animal type already defined. We might do this:

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

Delegated Methods

One of things Snit can do when components are defined explicitly is automatic method delegation. The example in the previous section showed how to delegate the dog's "eat" method to its animal component. Here's a simpler way:

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

Delegated Options

Just as you can delegate methods, so you can delegate options. In our dog example, dogs have an -akc flag, but all animals have a color. We could implement that like this:

% 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
black
Here, 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
black
As 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 -colour
And, as with methods, unrecognized options can be delegated as well:

    delegate option * to animal

Megawidgets

Tk widget names serve two purposes: the widget name is a Tcl command; it's also the name of a GUI window created by the Tk toolkit. Snit can create a command that looks like a widget name, but it can't create a GUI window; only Tk can do that. Thus, every snit::widget must be based (ultimately) on one or more real Tk widgets; and in particular the snit::widget's name must be the name of a Tk window.

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.

Components and Object Construction

The easiest way to write a constructor in the presence of components and delegation is to create all of the components without reference to the creation options, and then call "$self configurelist $args" at the very end. Sometimes this isn't possible; you might need to know the value of a particular option in order to create a particular component; indeed, the option might even specify what kind(s) of component to create. Suppose for example you want to write a megawidget that will add scrollbars to a widget of any type--you have to pass in the widget type of the widget to add scrollbars to. There are two ways to handle this case.

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 40
That'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.

Callback Code and Variables

Objects, and especially widgets, often allow other objects to register callback commands, or variables to be automatically updated. Care must be taken when a snit::type or snit::widget gives a command or variable to another object.

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:

Myself, I almost always use instance methods as callbacks; if they aren't methods that normal clients should be using, I give them capitalized method names to indicate that they are private.

Introspection

Starting with V0.6, Snit offers introspection into instances and types. Using the "$type info" and "$object info" commands (see below), it's possible to query the following kinds of information:

REFERENCE

Type and Widget Definitions

snit::type name definition
snit::widget name definition
Defines a new abstract data type or megawidget type called name. If name is not a fully qualified command name, it is assumed to be a name in the namespace in which the snit::type command appears (usually the global namespace). It returns the fully qualified type name.

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?
Defines a type variable with the specified name, and optionally the specified value. Type variables are shared by all instances of the type. This definition can be used to define array variables, but cannot initialize their elements.

typemethod name arglist body
Defines a type method with the specified name, argument list, and body. The variable "type" is automatically defined in the body to the type's fully-qualified name; the arglist may not contain the argument names "type" or "self".

Type variables defined in the type definition are automatically visible in the body of every type method.

option name ?defaultValue?
Defines an option for instances of this type, and optionally gives it an initial value. (The option's value defaults to "" if no initial value is specified.) The option's name must begin with a hyphen, "-". Options are normally set and retrieved using the standard configure and cget instance methods.

An option defined in this way is said to be "locally defined".

variable name ?value?
Defines an instance variable, a private variable associated with each instnace of this type, and optionally its initial value. This definition can be used to define array instance variables, but cannot initialize their elements.

method name arglist body
Defines an instance method, a subcommand of each instance of this type, with the specified name, argument list and body. 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".

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 constructor definition specifies code to be executed when a new instance is created. The arglist can contain any number of arguments, just like a method.

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
The destructor is used to code any actions which must take place when an instance of the type is destroyed: typically, the destruction of anything created in the constructor. The variables "type" and "self" are automatically defined in the body to be the type's and the instance's fully-qualified names

onconfigure name arglist body
Every locally-defined option has an "onconfigure" handler which is called when the option is set to a new value by the "configure" or "configurelist" instance method. The arglist may contain exactly one argument name, which may not be "type" or "self"; the argument receives the new value. The variables "type" and "self" are defined as usual in the handler's 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
Every locally-defined option has an "oncget" handler which is called when the option's value is retrieved. The variables "type" and "self" are defined as usual in the handler's body. Whatever the handler returns will be the return value of the call to the 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
Defines a new Tcl procedure in the type's namespace. The new proc differs from a normal Tcl proc in these ways: 1. All type variables defined in the type definition are automatically visible. 2. If the proc takes an argument called "self", and if that argument is passed the name of an instance of this type, then all instance variables defined in the type definition are automatically visible.

delegate method name to comp ?as compmethod compargs...?
Defines a delegated instance method. When instance method name is used with an instance of this type, it will automatically be delegated to the named component as though the method were defined as follows:

              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?
Defines a delegated option. When the 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.

The Type Command

A type or widget definition creates a type command, which is used to create instances of the type. The type command this form.

$type typemethod args....
The typemethod can be any of the standard type methods defined in the next section, or any type method defined in the type definition. The subsequent args depend on the specific typemethod chosen.

Standard Type Methods

In addition to any typemethods in the type's definition, all types and widgets will have at least the following method:

$type create name ?option value ...?
Creates a new instance of the type, giving it the specified name and calling the type's constructor.

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
Returns a list of the type's type variables (excluding Snit internal variables); all variable names are fully-qualified.

$type info instances
Returns a list of the type's instances. For snit::types, it will be a list of fully-qualified instance names; for snit::widgets, it will be a list of Tk widget names.

$type destroy
Destroys the type's instances, the type's namespace, and the type command itself.

The Instance Command

A snit::type or snit::widget's 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...
The method can be any of the standard instance methods defined in the next section, or any instance method defined in the type definition. The subsequent args depend on the specific method chosen.

Standard Instance Methods

In addition to any delegated or normal instance methods in the type's definition, all type instances will have at least the following methods:

$object configure ?option? ?value? ...
Assigns new values to one or more options. If called with one argument, an option name, returns a list describing the option, as Tk widgets do; if called with no arguments, returns a list of lists describing all options, as Tk widgets do.

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
Like 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
Returns the option's value.

$object destroy
Destroys the object, calling the 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
Returns the instance's type.

$object info vars
Returns a list of the object's instance variables (excluding Snit internal variables). The names are fully qualified.

$object info typevars
Returns a list of the object's type's type variables (excluding Snit internal variables). The names are fully qualified.

$object info options
Returns a list of the object's option names. This always includes local options and explicitly delegated options. If unknown options are delegated as well, and if the component to which they are delegated responds to "$object configure" like Tk widgets do, then the result will include all possible unknown options which could be delegated to the component.

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.

Commands for use in Object Code

Snit defines the following commands for use in object code: type methods, instance methods, constructors, destructors, onconfigure handlers, oncget handlers, and procs. They do not reside in the ::snit:: namespace; instead, they are created with the type, and are directly available.
varname name
Given an instance variable name, returns the fully qualified name. Use this if you're passing the variable to some other object, e.g., as a -textvariable to a Tk label widget.

typevarname name
Given an type variable name, returns the fully qualified name. Use this if you're passing the variable to some other object, e.g., as a -textvariable to a Tk label widget.

codename name
Given the name of a proc (but not a type or instance method), returns the fully-qualified command name, suitable for passing as a callback.

component name ?is obj
Used in its full form, it assigns an object command to a component logical name; as such it is usually used in a type's constructor, but it can be used in any instance code. The object command assigned to a component could change repeatedly in the lifetime of any object. Note: don't try this with ::snit::widget hull components.

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?
The 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
Normally, instance variables are defined in the type definition along with the options, methods, and so forth; such instance variables are automatically visible in all instance-specific code. However, instance code (e.g., method bodies) can declare such variables explicitly using the 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
Normally, type variables are defined in the type definition, along with the instance variables; such type variables are automatically visible in all of the type's code. However, type methods, instance methods and so forth can use 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.

HISTORY

During the course of developing Notebook, my Tcl-based personal notebook application, I found I was writing it as a collection of objects. I wasn't using any particular object-oriented framework; I was just writing objects in pure Tcl following the guidelines in my Guide to Object Commands, along with a few other tricks I'd picked up since. And it was working very well. But on the other hand, it was getting tiresome. Writing objects in pure Tcl is straightforward, once you figure it out, but there's a fair amount of boilerplate code to write for each one, especially if you're trying to create megawidgets or create objects with options, like Tk widgets have..

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.

CREDITS

Credit goes to the following people for using Snit and providing me with valuable feedback: Rolf Ade, Colin McCormack, Jose Nazario, and Jeff Godfrey.


Copyright © 2002, by William H. Duquette. All rights reserved.