Playing OO

Richard Suchenwirth 2002-10-03 - Oh-oh, we've done it so often - here I go again... Object orientation is a strong fashion in programming. There are several OO systems for Tcl, but the fact that none has taken Tclworld by storm seems to indicate that we are not as concerned with this fashion as followers of other languages are. Still, OO concepts ever again have a certain fascination. Let's look at fictitious examples:

 class Foo  ;# introduce a class "Foo" of objects
 Foo bar    ;# create an object of class "Foo" named "bar"
 bar say hi ;# invoke method "say" of class "Foo" for object "bar",
            ;# amounts to: Foo::say bar hi

and make them come to life, using namespaces and interp aliases. The idea is that every class has one namespace (to put its methods), and every object has a child namespace of its class' (to put its instance variables). For each class, its constructor is aliased to its name; for each object, its dispatcher is aliased to its name, and its "evaluator" to its name followed by a colon (see below). The following code evolved over a weekend and features (limited) multiple inheritance and garbage collection:

 namespace eval class {variable count 0 names ""}
 
 proc class {{name ""} {superclasses ""} args} {
    upvar #0 ::class::names names
    if {$name == ""} {return $names} ;# another introspection helper
    if {[lsearch $names $name]<0} {lappend names $name}
    # -- maybe inherit from superclasses
    set inheritedVars {}
    foreach superclass $superclasses {
        $superclass _ ;# temporary instance to ask for methods and vars
        foreach method [_ methods] {
            interp alias {} ${name}::$method {} ${superclass}::$method
        }
        foreach var [_ vars] {lappend inheritedVars $var [_: set $var]}
        _ delete
    }
    # -- inherit standard methods from 'class' to the new one
    foreach method {delete methods vars} {
        interp alias {} ${name}::$method {} class::$method $name
    }
    # -- The constructor is just called like the name of the class
    set args [concat $inheritedVars $args]
    interp alias {} $name {} class::new $name $args
 }
 proc class::new {class defaults {self ""} args} {
    if {$self == ""} {
        return [string map [list ::${class}:: ""]\
                     [namespace children ::$class]]
    }
    # -- make sure we're not clobbering an existing command
    if {[info command $self] != ""} {error "$self exists"}
    # -- if wanted, auto-create a unique object name
    if {$self == "#auto"} {variable count; set self $class#[incr count]}
    # -- create sugar for 'namespace eval' access
    interp alias {} $self: {} namespace eval ::${class}::$self
    # -- set the instance variables known so far
    namespace eval ::${class}::$self variable $defaults $args
    # -- if present, call custom constructor
    if {[info command ::${class}::new] != ""} {::${class}::new $self}
    # -- prepare garbage collection (see discussion below - turned off)
    #uplevel 1 "set $self ::${class}::$self"
    #uplevel 1 "trace add variable $self {write unset} {catch {$self delete} ;#}"
    # -- The dispatcher is just called like the name of the object
    interp alias {} $self  {} class::dispatch $class $self
 }
 proc class::delete {class self} {
    # -- if present, call custom destructor
    if {[info command ::${class}::del] != ""} {::${class}::del $self}
    # -- remove object namespace, hence all instance variables
    namespace delete ::${class}::$self
    # -- remove the two object aliases
    foreach i [list $self $self:] {interp alias {} $i {} {}}
    uplevel 1 "catch {unset $self}" ;# remove caller's reference
 }
 proc class::dispatch {class self {cmd methods} args} {
    # -- turn 'foo bar grill' into 'Class::bar foo grill'
    # -- Command name defaults to 'methods', as introspection help
    uplevel 1 [list ::${class}::$cmd $self] $args
 }
 proc class::methods {class -} {
    # -- make a list of methods available for class
    set prefix ::${class}::
    string map [list $prefix ""] [info commands $prefix*]
 }
 proc class::vars {class self} {
    # -- make a list of variable names available for object
    set prefix ::${class}::${self}::
    string map [list $prefix ""] [info vars $prefix*]
 }

Now testing... We create a class Boy, with custom constructor and destructor (which must be named "del", to preserve the Foo::delete alias) and some class methods, all in usual namespace notation; instantiate an object (both here and at class creation can instance variables with default values be specified) and try out all our new toys :-}

 class Boy  {}  arms 2 hairs 1000               ;# defaults for i. variables
 proc  Boy::new {self} {$self: variable legs 2} ;# another default
 proc  Boy::del {self} {puts "$self says goodbye..."}
 proc  Boy::say {self what} {
    puts "Hi, as a [namespace current] named $self I say: $what"
 }
 proc  Boy::showLegs {self {n ""}} {
    if {$n != ""} {$self: [list set legs $n]} ;# need [list] to wrap
    puts "I have [$self: set legs] legs"
 }
 #-------------------------------- Now playing around with it ...
 Boy sue age "42 +" hairs 500 ;# add new instance variable, and override one
 sue say "hello, world!"
 puts "[sue: set legs] legs"
 sue showLegs
 sue showLegs "exactly two"
 sue: set hair(beard) white
 puts [sue: array get hair]

Works, and is pretty lean: this "OO system" costs just a few procs in 40+ (pretty dense) lines of code... (I've almost doubled this figure by generously commenting what goes on, contrary to my habits ;-).

Classes inherit the generic constructor and destructor, but can also provide custom ones; objects inherit the generic dispatcher and evaluator (when called with trailing colon ":"). Instance variables can be introduced per class or object. Make sure that variables really exist in your namespace - otherwise you might end up in existing global variables instead. Using colon, we can execute all global commands in the Boy::sue namespace. This allows read/write/unset access to all instance variables - so I don't have to handle special cases of arrays, etc. You may consider "sue:" as a shorthand for "sue eval", that's how I started this, but the syntax

 object: set variable ?value?

reads so much nicer... Now testing object deletion:

 Boy shortlived
 shortlived: set tolive 0
 shortlived delete
 catch {shortlived: set tolive} res
 puts $res
 puts [info commands short*]

So what have we got here? A tiny framework for a class hierarchy (all classes inherit from class, and possible superclasses) where you can add or remove class methods or instance variables at any time, and introspect them with the methods and vars methods. No "private" or "protected" parts - this is more about freedom than encapsulation. You get most freedom (and save you and me work) by the namespace eval gateway, which exists in Tcl anyway, and the sweet sue: shorthand for it. One line of code buys us optional automatic object name generation, as known from incr Tcl. But as commands and namespaces go, all objects are global and persistent, so you have to delete them explicitly when done.

Looks like interp alias and namespaces indeed provide 95% of what's needed for (some flavor of) OO in Tcl...


"The tinkering then goes on for the rest of your life", as someone wrote about trains3.tcl. Here's how class inheritance (even multiple) is implemented: methods of superclasses are aliased, declared instance variables, with default values, are stored in the constructor alias. Note however that methods and vars are in sort of flat lists - if the superclasses have equally-named items, the last one wins. Also, the inherited methods and vars are a snapshot - if the superclasses later get more of them, they won't be automatically known to the subclass (but as shown below with the Truck::sound method, superclass methods can be called - as well as methods of any other class, e.g. Dog::sound...) Again, this is not about encapsulation, or preventing the programmer from doing certain things. Like before, you can do everything with Tcl, and this OO sugar just makes some things easier to write and read.

 #------------------------- testing inheritance ...
 class Car       {} wheels 4  motor gasoline  mph 100
 proc  Car::sound self {return honk} ;# will be overridden in test

 class Container {} volume ""  covered 1
 proc  Container::sound self {return rattle}

 class Truck     {Car Container} motor Diesel  mph 60
 proc  Truck::brake self {return screech!}

 Truck t1 volume 40m3  payload 30t  owner "John Smith"  wheels 6
 puts "Before: [t1 sound]"
 proc Truck::sound self {
    return [Car::sound $self],[Container::sound $self]
 }
 puts "After:  [t1 sound]"
 foreach var [t1 vars] {puts "$var: [t1: set $var]"}
 puts "Methods: [t1 methods]"

Works like expected again - after Tcl'ing for years, I'm yet again amazed by the power of the language, in this case the interp and namespace commands... and tinkering on...

Garbage collection means that objects are automatically deleted when no more needed. How long an object is needed, may be hard to tell, but one indication is: when the context it was created in is left. For this purpose I re-use a trick from Gadgets: associate a guard variable in caller's scope to the object, and call the destructor when the guard variable is deleted (explicitly, or on return) or assigned a value. For simplicity, the guard variable name is the name of the object; its value is the namespace name, so a simple but sufficient "runtime type information" (RTTI) is also provided. DISCLAIMER: Experiments in Playing OO design showed that this GC deletes too eagerly, if you don't want to litter global variables. Hence I commented the two lines out - back to explicit delete, sorry...

 # -------------------- Testing garbage collection:
 class Dog {} legs 4
 proc Dog::speak self {puts bow-wow!}
 proc testGC {} {
    global snoopy
    Dog snoopy size small
    [Dog fido] speak
    puts localDogs:[namespace children ::Dog]
 }
 testGC
 puts globalDogs:[namespace children ::Dog]

As the test example shows, you can prevent the automatic destruction by beforehand declaring the object name global - just as with variables (it is one!). After invocation of testGC, "snoopy" survives but "fido" has disappeared. Another example for look and feel, trying the short variable name I in place of self:

 class File   {} mode r  fp ""  name ""
 proc  File::new I {$I: set fp [open [$I: set name] [$I: set mode]]}
 proc  File::del I {::close [$I: set fp]}
 proc  File::<<  {I string} {puts [$I: set fp] $string}
 proc  File::>>  {I varName} {
    upvar 1 $varName var
    gets [$I: set fp] var
 }
 proc  File::close I {$I delete}
 #---------------------------------- testing again:
 File f name t.txt mode w
 f << "hello world!"
 f close
 File f name t.txt
 f >> input
 puts input:$input

In finishing touches, care was taken to make introspection easy:

 class ;# returns the list of defined classes
 Foo   ;# returns the list of objects of class "Foo"
 bar   ;# returns the list of methods for object "bar"

See Playing OO design for a more elaborate example.