Creating Object CommandsWhen you have to roll your own Contents
OverviewThere are several ways to model objects (data plus methods which act on that data) in pure Tcl. If you truly want to write object-oriented code, you can use one of the many OO extensions to Tcl; more typically, though, you're writing an extension which should stand by itself, and you want to give it an object-based flavor. The usual model is exemplified by Tk widgets: write a command that creates objects of the desired class. This constructor command creates a new Tcl command named after the object. To manipulate the object, use its command. By convention, the first argument to the object command specifies the method to execute; any remaining arguments are method specific. This document describes how such simple objects can be created. In particular, it discusses the basic or naive approach to creating object commands, and then a more robust approach which works properly in the context of namespaces and packages. The focus is on the mechanism by which the object command is created. Finally, it addresses how to pass arguments to the object's methods. This document explicitly does not cover advanced topics like object-oriented inheritance. To begin with, I have no wish to perpetrate yet another object-oriented framework. My fundamental assumption is that the programmer wishes to create an extension that has object semantics without requiring that the users of the extension pay the cost of a full-fledged object-oriented framework. The Basic ApproachThe basic approach to creating such an object is straightforward. Suppose you wish to create a "dog" object. Each dog should have a name and a color, and might be registered with the AKC. In addition, each dog should be able to bark. The dog's color and AKC flag should be set at creation, and retrievable later. For convenience, we will assume that the dog's name works as the name of a Tcl command; in practice this is unlikely to be so, and the dog's real name should be an attribute of the object. Storing the Data: Data about all dogs will be put in the
set DogInfo($name-color) $color
set DogInfo($name-akcflag) $akcflag
Handling Methods: We will need a private command to handle
all of the method invocations. The command will take the dog's name
as its first argument, and the method name as the second argument; if
any methods have additional arguments, an Our private command might look like this:
proc DogMethods {name method} {
global DogInfo
switch -exact -- $method {
color -
akcflag {
return $DogInfo($name-$method)
}
bark {
puts "$name says 'Woof!'"
}
}
}
This version of Creating the Object Command: We have data; we have methods; now we need a constructor to wrap them up together. Our constructor might look like this:
proc dog {name color {akcflag no}} {
global DogInfo
set DogInfo($name-color) $color
set DogInfo($name-akcflag) $akcflag
proc $name {method} "DogMethods $name \$method"
return $name
}
This procedure stashes the dog's data in the DogInfo array,
and then creates a new procecure, named after the dog, which
does nothing more than pass its name and argument to
Using the Object: Having made these definitions, we can create a dog object and use it as shown in the following dialog:
% dog Fido brown
Fido
% dog Montmorency gray yes
Montmorency
% Fido color
brown
% Fido bark
Fido says 'Woof!'
%
Problems with the Basic Approach: The basic approach illustrates the concepts adequately, but has some serious problems:
The Robust ApproachFirst, the object must be placed in its own package and namespace.
How to do so is adequately described in the
Guide to Success with Namespaces and Packages,
so I won't go into great detail; it suffices to say that the
The basic approach's remaining problems all stem from how the
object was named. Practically speaking, an object of the kind we are
discussing is simply a new Tcl command. As such, creating it should
be like creating a new command using
Following these rules has two important consequences: dog objects can
be protected by namespaces just as normal commands are, and every
dog object is guaranteed to have a unique name and therefore a
unique key into the With these points in mind, let's reimplement the Storing the Data: The data will be stored in an array, just as in the basic approach, except that the array will be placed in the "dog" namespace:
package provide dog 1.0
namespace eval ::dog:: {
# $name-color The dog's color
# $name-akcflag AKC, yes or no
variable DogInfo
# Export public commands
namespace export dog
}
Handling Methods: Methods are handled as in the basic
approach, except that the method handler lives in the "dog" namespace.
Also, note that
proc ::dog::DogMethods {name method} {
variable DogInfo
switch -exact -- $method {
color -
akcflag {
return $DogInfo($name-$method)
}
bark {
puts "$name says 'Woof!'"
}
}
}
Creating the Object Command: Creating the object command becomes much more involved. We must do the following things:
Qualifying the object's name: If the name begins with
"::", then it is already fully qualified. Otherwise, we must
determine the caller's namespace and append the object's name to it.
The
if {![string match "::*" $name]} {
# Get caller's namespace;
# append :: if not global namespace.
set ns [uplevel 1 namespace current]
if {"::" != $ns} {
append ns "::"
}
set name "$ns$name"
}
Checking the Name: Next, we must verify that the name is
unique. The
if {"" != [info commands $name]} {
return -code error "command name \"$name\" already exists"
}
Note that this use of Note the use of the I should end this section by saying that checking the command name for
pre-existence is somewhat controversial. Most of use are used to
sourcing the same file many times in a row while interactively testing
and debugging Tcl code; if the file creates a Putting It Together: Putting all this together, we end up with the following constructor:
proc ::dog::dog {name color {akcflag no}} {
variable DogInfo
# FIRST, qualify the name.
if {![string match "::*" $name]} {
# Get caller's namespace;
# append :: if not global namespace.
set ns [uplevel 1 namespace current]
if {"::" != $ns} {
append ns "::"
}
set name "$ns$name"
}
# NEXT, Check the name
if {"" != [info commands $name]} {
return -code error "command name \"$name\" already exists"
}
# NEXT, Save the data
set DogInfo($name-color) $color
set DogInfo($name-akcflag) $akcflag
# NEXT, Create the object.
proc $name {method} "::dog::DogMethods $name \$method"
return $name
}
Using the Object: Having made these definitions, we can now create dog objects in different namespaces, as shown in the following dialog:
% dog::dog Fido brown
::Fido
% namespace eval Foo {
dog::dog Fido gray yes
}
::Foo::Fido
% Fido color
brown
% Foo::Fido color
gray
% namespace eval Foo {Fido bark}
::Foo::Fido says 'Woof!'
% dog::dog Fido white
command name '::Fido' already exists
% puts $errorInfo
command name '::Fido' already exists
while executing
"dog::dog Fido white"
%
Adding Method ArgumentsWe now have a
Moreover, the method code is likely to be longer than will reasonably
fit in the body of a single To aid in the examples, we will define two additional methods,
Passing the Arguments: The object command should collect any
arguments following the method name and pass them along to
proc $name {method args} "::dog::DogMethods $name \$method \$args"
Next,
proc ::dog::DogMethods {name method argList} {
.
.
.
}
Method Procedures: Each method will get its own method procedure. The procedure's name will be the method name, prefixed by "Op"; this prefix allows method names like "set" to be used without confusion. The procedure will take whatever arguments it needs, beginning with the name of the object; this has the advantage that Tcl will do most or all of the argument checking for us. If a method needs three arguments, Tcl will raise an error if it is passed two or four arguments. The following is the
proc ::dog::Opcolor {name} {
variable DogInfo
return $DogInfo($name-color)
}
This method takes no arguments, and so the procedure has only one
argument, the object's name. The new
proc ::dog::Opchase {name quarry} {
puts "$name chases after $quarry."
}
Finally, the new
proc ::dog::Opgetdata {name arrayName} {
variable DogInfo
upvar 3 $arrayName theArray
array set theArray [list color [Opcolor $name] akcflag [Opakcflag $name]]
return
}
Note here the use of Note also that a method routine can call the other methods without
qualification, as Dispatching the Methods: The procedure
if {[catch "Op$method $name $argList" result]} {
regsub -- "Op$method" $result "$name $method" result
return -code error $result
} else {
return $result
}
First, we call the method procedure, catching any error. The
script argument to the Putting It Together: Putting all this together, we now have the following code:
package provide dog 1.0
namespace eval ::dog:: {
# $name-color The dog's color
# $name-akcflag AKC, yes or no
variable DogInfo
# Export public commands
namespace export dog
}
proc ::dog::dog {name color {akcflag no}} {
variable DogInfo
# FIRST, qualify the name.
if {![string match "::*" $name]} {
# Get caller's namespace; #
# append :: if not global namespace.
set ns [uplevel 1 namespace current]
if {"::" != $ns} {
append ns "::"
}
set name "$ns$name"
}
# NEXT, Check the name
if {"" != [info command $name]} {
return -code error "command name \"$name\" already exists"
}
# NEXT, Save the data
set DogInfo($name-color) $color
set DogInfo($name-akcflag) $akcflag
# NEXT, Create the object.
proc $name {method args} "::dog::DogMethods $name \$method \$args"
return $name
}
proc ::dog::DogMethods {name method argList} {
variable DogInfo
switch -exact -- $method {
color -
akcflag -
bark -
chase -
getdata {
if {[catch "Op$method $name $argList" result]} {
regsub -- "Op$method" $result "$name $method" result
return -code error $result
} else {
return $result
}
}
default {
return -code error "\"$name $method\" is not defined"
}
}
}
proc ::dog::Opcolor {name} {
variable DogInfo
return $DogInfo($name-color)
}
proc ::dog::Opakcflag {name} {
variable DogInfo
return $DogInfo($name-akcflag)
}
proc ::dog::Opbark {name} {
puts "$name says 'Woof!'"
}
proc ::dog::Opchase {name quarry} {
puts "$name chases after $quarry."
}
proc ::dog::Opgetdata {name arrayName} {
variable DogInfo
upvar 3 $arrayName theArray
array set theArray [list color [Opcolor $name] akcflag [Opakcflag $name]]
return
}
Using the Object: Having made these definitions, we can now call methods with arguments, and get appropriate error messages if we call them in error:
% dog::dog Fido brown
::Fido
% Fido color
brown
% Fido color black
called "::Fido color" with too many arguments
% Fido chase Fluffy
::Fido chases after Fluffy
% Fido chase
no value given for parameter "quarry" to "::Fido chase"
% Fido getdata myArray
% parray myArray
myArray(akcFlag) = no
myArray(color) = brown
%
Cleaning Up the Stack TraceWhen the
% dog::dog Fido brown
% Fido color black
called "::Fido color" with too many arguments
% puts $errorInfo
called "::Fido color" with too many arguments
while executing
"::dog::DogMethods ::Fido $method $args"
(procedure "Fido" line 1)
invoked from within
"Fido color black"
%
The This may seem like a minor problem; if it offends you, it is easily
handled, though it makes the object command a little more
complicated. Replace the
proc $name {method args} [format {
if {[catch {::dog::DogMethods %s $method $args} result]} {
return -code error $result
} else {
return $result
}
} $name]
Note the use of the In the new version, the object command catches any errors from
lower levels and raises them again using
% dog::dog Fido brown
% Fido color black
called "::Fido color" with too many arguments
% puts $errorInfo
called "::Fido color" with too many arguments
while executing
"Fido color black"
%
AcknowledgementsI created this guide as a memory-aid for myself, but also as a means of learning from those more knowledgeable as myself. I'd particulary like to mention Donald G. Porter for his helpful comments and suggestions; thanks largely to his input, this is a much more thorough guide than I had originally intended. If you have questions, comments, or suggestions about this page, feel free to contact me at will@wjduquette.com. |
|