This document describes
Tod, an implementation of Object Oriented features using Tcl namespaces.
The specific features are encapsulation,
inheritance, virtual functions and delegation.
The implementation is highly compact (under 400 lines of Tcl) and portable
to anywhere Tcl 8.3+ is.
The mechanism used is exceedingly simple, and can be expressed
in just a few lines of code.
Moreover, code written using Tod can be validated using Weld.
And, Tod is distributed under the same BSD license as Tcl uses.
2. Overview
Tod imports just 3 commands into the global namespace:
New: create a new object.
Delete: destroy an object.
Newv: create an object and assign it to a variable.
Other less frequently used commands are called with the prefix Tod::,
or (as with mc, method, and Opts)
defined externally and/or emulated.
An application creates a namespace, with object data
initializers in array _ and optionally with the methods ~New/~Delete.
It can declare methods using either method or proc
with an extra first arg. (see below).
Following is a small excerpt using Tod:
package require Tod
namespace eval ::simple {
variable _
array set _ { a 0 b 1 c 2 };
method inc {n} { incr n }
method dec {n} { incr n -1 }
method ~Delete {args} { }
method ~New {} {
$_ dec $(a); # Dispatch call.
dec $_ $(a); # Direct call.
set (b) [$_ inc $(a)]
}
}
set o [New ::simple]
$o inc -1
$o dec 1
Delete $o
The New command creates an object which is both a command
and data (array).
Method's are just procs that takes the object name "_" as the first argument,
and add an "upvar $_ {}".
Inside methods,
data elements of the object (ie. array) are accessed using the notation $(element).
The object _ also can dispatch or send messages.
A constructor [~New] and/or destructor [~Delete] may also be defined
for the namespace.
Note: All namespace-local Tod methods and variables are prefixed with a tilde "~" character.
3. Options
Tod also includes support for pre-defining options, arguments accepted
by the args argument of ~New.
This implicitly uses the Opts module command, if available.
The format of an options list is a list of lists, where each sublist
describes one option using the form: { NAME DEFAULT DESCRIPTION ...}.
Only the first (NAME) is required, and values beyond the third
are paired values which are
unused in the Tod's emulated version of Opts.
Here is an options example.
package require Tod
namespace eval ::todtest {
variable _
array set _ { a 1 sub {} };
variable Opts {
{ -n 0 "Start value" }
{ -m -1 "End value" }
{ -debug 0 }
}
method func {n args} {
Opts p $args {
{ -x 0 "The x offset" }
{ -y 0 }
}
return [expr {$n+$(a)+$p(-x)*$p(-y)}]
}
method ~Delete {args} {
# The destructor (optional)
}
method ~New {args} {
# The constructor (optional)
set (a) [expr {$(b)+1.0}]
$_ func 3 -x 1
func $_ 4 -y 2 -x 21
}
}
set o [New ::todtest -n 1]
One advantage of Opts is it provides a backward compatible way to extend
procs
in the future.
3.1 The -icfg Option
The -icfg option provides
a simple way to set object elements without cluttering up $Opts with umpteen options.
The -icfg argument option, if defined, stores name/value pairs into the object.
namespace eval ::labentry {
variable _
array set _ {
bg white
fg black
}
variable Opts {
{-title {} "Title for widget" }
{-icfg {} "Internal config option pairs" }
}
'^
method ~New {args} {
pack [label .l -bg $(bg) -fg $(fg) -text $(-title)] -side left
pack [entry .e -bg $(bg) -fg $(fg)] -side left
}
}
New ::labentry -title "Hello World" -icfg "fg white bg black"
4. Inheritance
Static inheritance can be implemented simply by using namespace import, as in the following:
Subsequently, all methods in ::a are now also in ::b. Moreover, the call
$_ foo -4 in a::bar becomes a virtual call to b::foo.
One limitation of the above is that
it inherits code but not data. To include data, use Tod::Inherit.
namespace eval ::b {
variable _
variable Opts {
{ -n 0 "The start number" -type int }
{ -m -1 "Count of chars" }
}
Tod::Inherit ::a
array set _ { x 0 y 0}
}
The Inherit command is roughly equivalent to:
namespace eval ::b {
# ...
namespace import -force ::a::*
array set _ [array get ::a::_]
array set _ { x 0 y 0}
eval append Opts $::a::Opts
# ...
}
When data is not an issue,
an alternative approach is to use implicit importing via Uses as in:
namespace eval ::b {
uses ::a
}
This provides dynamic import, but only for those sub-commands
which get called.
For dynamic inheritance, see Dispatcher.
5. Directives
Tod supports several control directives, which are looked for in the _ array.
These directives, which all begin with the prefix ~tod-, are as follows
(given in example form):
set _(~tod-ndebug) 1 ;# Disable all warnings for options, chk*, etc.
set _(~tod-strict) 1 ;# Escalate all warnings to errors.
set _(~tod-nocatch) 1 ;# Do not use catch for ~New, ~Delete or ~Clone.
set _(~tod-chkinit) 1 ;# Warn of writes to uninitialized data members.
set _(~tod-chkmatch) _* ;# Pattern of data members to ignore (string match).
set _(~tod-chkregexp) ^-.* ;# Pattern of data members to ignore (regexp)
Directives (even unrecognized ones) are not assigned into the object array.
Tod directives may also be set globally using env eg: "set env(TOD_OPTS) {ndebug 1 strict 0}".
Note: a current limitation of directives controlling warnings (ndebug and strict) is that object specifics apply only to New and data write checks. The global defaults will be used for Delete and Clone.
The ~tod-chkinit directive is used check writes to the object array.
By default, any element can be added, but using chkinit will setup
write trace to verify that writes are to elements that were
initialized (including options). In addition, if either chkmatch or chkregexp
are given, elements matching those patterns are accepted as well.
6. OO in plain Tcl.
The decision to use OO in Tcl can be a difficult one to take.
Part of the problem is that using a non-standard
Tcl syntax makes it incompatible with existing Tcl tools (such as procheck).
This is a problem because any reasonably large Tcl application will likely
want access to these tools. Therefore, a principle objective of Tod is to achieve
OO-like power, without giving up the advantages of plain Tcl.
It achieves this by allowing use of ordinary procs with the
object passed explicitly:
For example, consider the following snippet of code, which is both valid Tcl and valid Tod:
namespace eval ::simple {
variable _
array set _ { a 1 b 2 };
proc addn {_ n} {
upvar $_ {}
incr (a) $n
}
proc ~New {_ args} {
upvar $_ {}
set (a) [expr {$(a) * $(b)}]
addn $_ 10
}
# Epilogue with self-test.
if {$argv0 == [info script]} {
array set _ $argv
eval ~New [namespace current]::_ $argv
}
}
It is even possible to use Tod code without Tod.
In the above example
the epilogue portion provides a self-test for the module.
Note that neither ::Tod nor any other package is used in the above.
Yet an object is passed around to several methods.
However the limitation of this code is that dispatch is unsupported.
To remedy this, change the epilogue to add an alias:
array set [set o [namespace current]::_tod_1] [concat [array get _] $argv]
interp alias {} $o {} Dispatch $o [namespace current]
proc ::Dispatch {_ ns cmd args} { uplevel 1 [linsert $args 0 ${ns}::$cmd $_] }
eval ~New $o $argv
This allows messages to be dispatchable via objects, eg: $_ addn 10.
A few more lines can add New/Delete wrappers, Opts, etc.
But the point is that in 3 short lines, one can
implement the mechanism that Tod uses.
There's no black-box, just a logical mapping, openly explainable.
7. Summary
Within a namespace, the following variables are specific to Tod:
{} - object array, inside methods is accessed via $().
_ - if inside method: the object name, outside: the object initializer array.
TOD defines an object as an array-variable/command-alias pair.
That is, the array and command-alias share the same name.
The object is passed as a the first argument to all methods.
The object may be thought of as an instance variable of a namespace
(aka class) wherein procs (aka methods) and data may be defined.
Also, simple inheritance is supported via [namespace import].
And ultimately, TOD is designed to simplify coding and maintaining
Tcl applications.
As a result, little regard is paid to other OO
systems because in general, OO is not the goal, it is the means.
TOD is component that evolved out of the development of TED and THT.
As such, it is included with Module (ie. comes with package require Module).
At around 300 lines of code, TOD is small enough to
be easy to learn, use and best of all modify.
And, full validation of TOD code is available using WELD, the
commercial Tcl validator and debugger.
8. Command Reference
Following are brief descriptions of commands in Tod.
For authoritative details, refer to the code.
8.1 New
Calling New NS ... is used to create an object for a namespace
(or if passed an object, clone it).
With no NS arg, the callers current namespace is used. If NS does not begin
with :: then it is assumed to be relative to the current NS of the caller.
And in order for args to be passed to New, a [~New] constructor
must have been defined with matching args. If ~tod-ndebug=1, then catch is used
in the call to ~New to cleanup in case of an error.
If the constructor wishes, it can force New to return it's result
(rather than the object) by using return -code return.
However, it should also make a call to Delete just prior to doing so if it
wishes the ~Delete destructor to be called.
This so-called self-destructing object,
may not be used with Newv.
For example, in the following, bgexec is a Tod implemention such that -wait causes
command to wait for results then clean itself up before returning the result.
ie. no object is returned.
set rc [New ::pdqi::bgexec "ls -lR" -wait 1]
# this replaces the following...
set o [New ::pdqi::bgexec "ls -lR"]
set rc [$o data]
Delete $o
If there is ambiguity, the caller can determine that a returned result was an object by
using: Tod::IsObject $rc.
8.2 Newv
Newv calls New and then assigns the result to a variable.
When subsequently the variable gets modified or deleted, the
object will automatically be deleted.
This is most useful for instantiating auto-cleaning sub-objects. eg.
method MyMeth {args} {
Newv (obj) ::Foo
Newv (obj2) ::Bar
#...
Delete; # Chains deletion of Foo and Bar as well.
}
Newv is implemented using a variable trace.
8.3 Delete
Delete (besides invoking [~Delete] if defined) destroys an object,
cleaning up the command-alias and array data. Arguments to Delete
are passed to the destructor and the returned value become the result of Delete.
In order for Delete to be validly called with arguments, a
destructor ([~Delete])
must have been defined with corresponding args.
Duplicate calls to Delete for already deleted objects will be ignored.
If ~tod-ndebug, then catch/error is used in invoking ~Delete to ensure cleanup.
8.4 method
method provides a modified version of proc to add the object argument
and upvar. This is implemented simply as:
Be aware that although method definitions are cleaner looking than
declarations with proc,
there are disadvantages to doing so in larger systems. For example: proc code works with existing
Tcl tools such as procheck. So caution should be exercised when
choosing method over proc..
Also of note is that method not part of the Tod namespace. That's because
Weld define method as a C command in order to support it's compilation.
8.5 Opts
Opts provides a facility for initializing a local array, first from
an options list, then from args.
eg:
method foo {x y args} {
Opts p $args {
{ -start 0 "The starting value" }
{ -end -1 "The ending position" }
{ -- }
}
if {$p(-start) > $(start)} { ... }
# Process trailing data...
foreach i $p(--) { ... }
}
The above would have a similar effect to:
array set p { -start 0 -end -1 }
array set p $args
With the following differences:
Only options listed will be assigned: other unknown options issue warnings.
The '' option (when specified as the last option) is used to mark the end of options and gets assigned with the trailing values in args.
Opts is used implicitly by New when the namespace defines a variable Opts.
Arguments passed in the args parameter of ~New automatically get processed
and initialized in the object.
And options in an object may subsequently be modified by using Tod::Configure.
Note: When run standalone, TOD provides it's own minimal version of ::Opts.
However, when run as part of Module a more sophisticated version is provided
to include type validation and other goodies. Also, $Opts and [Opts] are used
in the generation of extern definitions.
8.6 Clone
Clone is normally called via New.
A clone occurs when New is called with an object instead of a namespace.
A [~Clone] method may also be defined that gets called during the clone.
It is passed the
old object as an argument eg.
namespace eval ::x {
method ~Clone {oldobj} {
upvar $oldobj o
lappend (clones) $oldobj
set o(master) $_
}
}
set o [New ::x]
set o2 [New $o]
8.7 Dispatcher
Code is used to install a custom dispatcher,
overriding the current one in an objects command-alias.
The Dispatcher command installs the new command and returns the old. eg.
proc Delegator {_ ns cmd args} {
# Custom dispatcher to delegate to ns or sub-namespaces in (~subns).
if {[namespace which -command [set ocmd ${ns}::$cmd]] == {}} {
foreach ns $(~subns) {
if {[namespace which -command [set ocmd ${ns}::$cmd]] != {}} break
}
}
uplevel 1 [linsert $args 0 $ocmd $_]
}
method ~New {args} {
Tod::Dispatcher $_ [namespace current]::Delegator
}
As delegation is in common use, Tod provides a utility proc
(similar to the above). ie.
Note: The above are available in Tod as Tod::Delegator and Tod::Delegatees.
8.8 IsObject
Return 1 if _ is an object.
8.9 Configure
Convenience proc for setting options in an object that were defined in $Opts.
8.10 Inherit
Inherits code and data from each namespace in list.
8.11 Catch
By default, Tod Dispatch will not handle using [return -code] properly.
To do so requires installing a catch based dispatcher,
by calling [Tod::Catch $_] usually from ~New.
If optional suffix is given, a new alias is created, allowing both methods to
be used at once. Otherwise, the existing alias is modified.