Tcl Tutorial Lesson OOP1

Object-oriented programming 101

Since version 8.6 Tcl has a framework for object-oriented programming, meant both as a full-fledged OOP system and as the basis for more extensive OOP systems. This is part of a long tradition: Tcl has long been flexible enough for people to build their own OOP system on top of, leading to a plethora of such systems, with very diverse functionality, to name a few: IncrTcl, XoTcl and Snit. Here we take a look at the basic OOP features. Much more can be found for instance in the book The Tcl Programming Language by Ashok Nadkarni, or in the online chapter .

Dealing with global variables

Consider the following code:

    proc range {cmd {arg {}}} {
        global RANGE
        switch -- $cmd {
            "max" {
                set RANGE(max)     $arg
                set RANGE(current) -1
            }
            "next" {
                upvar $arg var
                incr RANGE(current)
                set var $RANGE(current)
                if { $var < $RANGE(max) } {
                    return 1
                } else {
                    return 0
                }
            }
        }
    }

This command is meant to generate a series of integer values, so that we can iterate over them. This could be quite useful in a numerical context. (Of course we can also use the for command.) We can use it like this:

    range max 5
    while { [range next idx] } {
        puts $idx
    }

and produce the numbers 0, 1, ... 4.

While the implementation above does not take care of error handling for simplicity - normally you should make your code robust enough - it has a much larger flaw: you can not use it in a "nested" way because of the use of a global array (RANGE) to store the state. You might be able to avoid that in your own code, but what if you were to use a library from somebody else that also uses the command?

Try the code below (the intention is of course to have x and y both run from 0 to 9):

    range max 10
    while { [range next x] } {
        while { [range next y] } {
            puts [expr {$x + $y}]
        }
    }

That is where object-oriented programming techniques come in. You can store the state inside an "object" and this state is not easily accessible outside the interface to the object.

Classes and objects as an alternative

So, here is one way you can turn the above range command into a class for creating "range" objects from:

    ::oo::class create range {
        variable max 
        variable current

        constructor {upto} {
            my max $upto
        }

        method max {upto} {
            variable max 
            variable current
            set max     $upto
            set current -1
        }

        method next {varName} {
            upvar $varName var
            incr current
            set var $current
            if { $var < $max } {
                return 1
            } else {
                return 0
            }
        }
    }

The code contains the two subcommands from the original range command, but also a constructor. This method is called when the object is created. To avoid duplication of code it calls the max method on itself. Hence it is called via the my command.

Analogously, if an object is destroyed, you may need to free the resources it is holding - that is the task for the destructor. It is defined via:

    ::oo::class create range {
        ...

        destructor {
            puts "Destroying [self]"
        }
    }

Destructors take no arguments. If you do not define a destructor for the class, default destructor is provided.

An important difference is that each method has its own argument list, whereas with the original range command the argument list has to work for all subcommands and so is rather abstract. In a command with limited functionality this is not that bad, but suppose you have 10 different subcommands. The logic would get quite involved.

With this class we can create any number of range objects that work independently:

    set xrange [range new 10]
    set yrange [range new 10]

    while { [$xrange next x] } {
        while { [$yrange next y] } {
            puts [expr {$x + $y}]
        }
        $yrange max 10 ;# We need to reset the y-range
    }

The command $yrange max 10 is necessary to reset the yrange object. Otherwise it would simply keep returning 0 and nothing would happen. This is a bit awkward, so let us add a command that resets the range objects. We can extend the class by either adding the new method definition to the class definition above or through the oo:define feature command, which allows us to incrementally build up a class definition:

    ::oo::define range method reset {} {set current -1}

This causes a new method, reset, to become available for all instances of this class, even the ones that have already been created at that point.

Inheritance

TclOO allows for inheritance and even multiple inheritance. As an alternative we could create an expanded class which has the reset method:

    ::oo::class create newrange {
        superclass range
        method reset {} {set current -1}
    }

The superclass command arranges that the new class inherits the methods and variables from the class range. If you want multiple inheritance, list two or more classes as arguments to this command.

Sometimes it is useful to create an object with a specific name. Use className create in stead of className new:

    range create xrange 10

creates an object that van be invoked as xrange instead of $xrange.

Cloning objects

Another useful feature is that you can clone an object:

    ::oo::copy xrange yrange

create a new object, yrange with the same methods and variables that have the same values as the object xrange.

Advanced features

TclOO thus can be used in a similar fashion as class in C++ or Java, but there is more. You can add methods and variables to an object instead of a class. In that case, the object has the original methods and variables, just as any object from that class, but also extra methods and variables, possibly unique to the object:

    ::oo::objdefine $yrange {
        method reset {} {
            my variable current
            puts "resetting ..."
            set current -1
        }
    }

Due to the scoping rules, which become somewhat involved with inheritance and mixins, you need to explicitly import the object variables via the my variable command, like shown above. The variable command that is used within the context of namespaces performs a similar but not identical task.

Further features that are worth exploring are:

  • The use of mixins, classes that are "mixed into" the object
  • Filters that can be interposed in the method calls
  • Introspection via the self command
  • Invoking methods in the superclasses

These topics and others are summarised in More on Object-oriented programming.