Recent Changes - Search:


Mod /

Tod

(:toc:) (:toc:)

TOD - Tcl Object Dispatch (v2.0)

This document describes Tod, which provides a object-like features using Tcl namespaces. The specific features are encapsulation, inheritance, virtual functions and delegation. Its implementation is highly compact (under 400 lines of Tcl) and portable to anywhere Tcl 8.3+ is. The delegation 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 Wize. And, of course, Tod is distributed under the same BSD license as Tcl uses.

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

(:showex div=todex:)

Following is a small excerpt using Tod:

    package require Tod

    namespace eval ::simple {

        variable _
        array set _ { a 0  b 1  c 2 };

        proc inc {_ n} { incr n }

        proc dec {_ n} { incr n -1 }

        proc ~Delete {_ args} { }    

        proc ~New {_} {
            upvar $_ {}
            $_ 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). You can use method instead of proc. 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.

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.

(:showex div=optex:)

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 } 
        }

        proc func {_ n args}    {
            upvar $_ {}
            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.

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.

(:showex div=icfgex:)

    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"

Inheritance

Static inheritance can be implemented simply by using namespace import, as in the following:

(:showex div=inhex:)

    namespace eval ::a {
        namespace export *
        method foo args { tclLog FOO(a); }
        method bar args { tclLog BAR(a); $_ foo -4 }
    }

    namespace eval ::b {
        namespace import -force ::a::*
        method foo a {
            tclLog FOO(b)
            if {$a>0} {
                bar $_ -1
                $_ foo -3
                a::foo $_ -2
            }
        }
    }

    set o [New ::b]
    $o foo 1
    Delete $o

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.

(:showex div=index:)

 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.

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.

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:

(:showex div=simpex:)

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:

(:showex div=epi:)

  if {[catch { package require Tod }] {
    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 $_] }
  }

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.

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-*) - directives for tod.
  • Opts - list of options for args in ~New

The defined proc signatures are:

    proc Clone {_ {interp {}}}
    proc Configure {_ args}
    proc Delegatees {_ {nslst {}}}
    proc Delegator {_ ns cmd args}
    proc Delete {{_ {}} args}
    proc Dispatcher {_ {newcmd {}} {interp {}}}
    proc Dispatch {_ ns cmd args}
    proc IsObject {_ {exists 1}}
    proc New {{ns {}} args}
    proc Newv {var args}
    proc Opts {var vals lst args}
    proc method {name arglst body}

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

Command Reference

Following are brief descriptions of commands in Tod. For authoritative details, refer to the code.

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.

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.

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.

method

method provides a modified version of proc to add the object argument and upvar. This is implemented simply as:

    proc method {name arglst body} {
        uplevel 1 [list ::proc $name "_ $arglst" "upvar \$_ {};$body"]
    }

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 Wize define method as a C command in order to support it's compilation.

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.

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]

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.

    method ~New {args} {
        Tod::Delegatees $_ { ::MidNs ::BotNs }
    }
    method ~Delete {args} {
        Tod::Delegatees $_
    }

Note: The above are available in Tod as Tod::Delegator and Tod::Delegatees.

IsObject

Return 1 if _ is an object.

Configure

Convenience proc for setting options in an object that were defined in $Opts.

Inherit

Inherits code and data from each namespace in list.

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.

(:showex div=catchex:)

  proc Foo {} {
      return -code 2
  }
  method ~New args {
      set (oo) [Tod::Catch $_ cat]
      # $_ Foo
      $(oo) Foo
      set (neverexecuted) 1
  }

  method ~Delete args {
     interp alias {} $(oo)  {}
  }

  method 


Edit - History - Print - Recent Changes - Search
Page last modified on September 16, 2010, at 07:37 AM