Introducing Tcl 8.7 Part 8: TclOO

Published

This is the eighth in a series of posts about new features in the upcoming version 8.7 of Tcl. It describes several enhancements to Tcl's features for object-oriented programming.

To take Tcl 8.7 for a spin, you can download the source distribution. Binary distributions for Windows are available from magicsplat and BAWT.

NOTE: If you are not familiar with the object-oriented programming features in Tcl 8.6, see this tutorial.

These enhancements, courtesy TCT member Donal Fellows, include

  • Private variables and methods
  • Inline export / unexport modifiers
  • Additional slot operations
  • Class variables and methods
  • Singleton and abstract classes
  • Convenience commands for common operations
  • Hooks for extending the class and object definition language
  • Utilities for creating custom definition dialects

Some of the above do not introduce any new capabilities into Tcl. Rather they make some programming idioms more convenient and formalize some implementation dependent behaviors.

Describing all the above would make for a rather lengthy post so this post only covers the first three. The remaining will be the topic of a future post.

Private variables and methods

A limitation of TclOO in 8.6 was that methods were either exported (aka public), which made them callable from all contexts, or not exported which made them callable only from within the object's context. The latter includes all methods defined for that object including those in subclasses, mixins, filters etc. This broke isolation between class definitions.

  • An author of a subclass had to be careful when defining a method (even unexported) not to give it the same name as a method in the superclass.

  • At the same time, if the superclass implementation changed, necessitating the addition of a new method for private use, there is the risk that it will be hidden by a method of the same name in a subclass. Since the two classes may be written by different authors or teams, and the superclass may not be even aware of the existence of the subclass that uses it, this is a real risk.

Here is an example, contrived as always. We have a Service class which acts as a base for various kinds of services (daemons for the Unix weenies), database, network and so on. It has stop and abort methods which end the service both of which call Terminate to exit.

oo::class create Service {
    method Terminate {code} { puts "Exiting with code $code" }
    method stop {} { my Terminate 0 }
    method abort {} { my Terminate 1 }
}

The service is obedient and stops when told.

% Service create service
::service
% service stop
Exiting with code 0

Now along comes a programmer who appreciates the extensive functionality of our class and extends it to write a network server.

oo::class create NetworkService {
    superclass Service
    method accept {client} { blah blah }
    method disconnect {client} {
        # Do some cleanup
        my Terminate $client; # close connection
    }
    method Terminate {client} {
        puts "Closing client connection"
    }
}

Unfortunately, now the service does not stop when commanded to do so. It tries to close some imaginary client connection instead.

% NetworkService create netservice
::netservice
% netservice stop
Closing client connection

The problem of course is that the utility method Terminate has been unknowingly overridden. There is no isolation provided for the parent class from any of its child classes.

In Tcl 8.7, you can define private methods to fix this. Calls to a method marked private will never be overridden in the context of the class that defines that method.

Let us try it out in our example. Instead of rewriting the class, we'll just do some targeted surgery to redefine the method as private.

% oo::define Service {deletemethod Terminate}
% oo::define Service {private method Terminate {code} {puts "Exiting with code $code"}}
% netservice stop
Exiting with code 0

As an aside, notice TclOO's dynamic nature — the existing netservice object also picked up the corrected behaviour.

Some additional points:

  • Variables suffer from similar problems for similar reasons. A class may inadvertently name a variable that is already in use for a different purpose in a superclass or mixin. This is also resolved by defining variables as private variable analogously to private methods.

  • Methods that are forwards can also be defined with the private designation.

  • Private methods are not just for classes but can also be defined for objects though they are less useful there as objects themselves cannot be derived from.

  • Just like oo:define, private can either be used to define a single method as above or take a single argument which is a script within which multiple methods, forwards and variables are defined. This is convenient for clubbing together the entire private section of a class.

  • The info class and info object introspection commands take a new -scope option.

% info class methods Service -scope private
Terminate
% info class methods Service -scope public
abort stop
% info class methods Service -scope unexported
% 

The unexported value corresponds roughly (but not exactly!) to protected in C++ terminology. They are not visible from outside the object's context but because they are not private, they are callable from methods defined in subclasses. We did not have any in our example.

See TIP 500 for more examples of private methods and variables.

Inline export modifiers

In Tcl 8.6 methods were exported by default based on whether they started with a lowercase letter or not. You could change the visibility of a method using the export and unexport commands. For example,

oo::class create Users {
    variable users
    method +user {name address} { set users($name) $address}
    method -user {name} { unset -nocomplain users($name) }
    export +user -user
}

Because they do not begin with a lower case letter, the methods to add and remove users have to be explicitly exported. Separating the visibility from the method definition is somewhat of an irritation so Tcl 8.7 allows you to modify it with the -export option in the method definition itself.

oo::class create Users {
    variable users
    method +user -export {name address} { set users($name) $address}
    method -user -export {name} { unset -nocomplain users($name) }
}

Conversely, there is an -unexport option that will hide a method that would be exported by the default rules.

New slot operations

First, some background. One of the special features of Tcl's OO system is that unlike other languages it is completely dynamic in keeping with Tcl's philosophy. Class structures, relationships, object types, interfaces can all be manipulated even at runtime.

Continuing our tradition of silly examples, suppose our web server defines a request class as follows. All methods are guarded by a filter that denies access based on client location.

oo::class create Request {
    variable client_location
    constructor {location} {
        set client_location $location
    }
    method get {url} {return "$url content"}
    method head {url} {return "$url head"}
    method Banned {} { return [expr {$client_location ne "earth"}] }
    method LocationFilter {args} {
        if {[my Banned]} {
            return "Go away!"
        }
        next {*}$args
    }
    filter LocationFilter
}

It all works as we want. Content is delivered only for earthlings.

% Request create greystroke earth
::greystroke
% Request create carter mars
::carter
% greystroke get lord.of.the.apes
lord.of.the.apes content
% carter get princess.of.mars
Go away!

Now we decide, based on some change in the system configuration, that requests need to be logged.

oo::define Request {
    method Log {args} {
        puts "LOG: [self target]: $args"
        next {*}$args
    }
    filter Log
}

But now we see a problem. Successful requests are logged but the denied ones aren't.

% greystroke head lord.of.the.apes
LOG: ::Request head: lord.of.the.apes
lord.of.the.apes head
% carter get princess.of.mars
Go away!

The problem is that the Log filter was appended to the list of filters while we need it to be the first. In Tcl 8.6, there was no way to do this but Tcl 8.7 provides a -prepend option for this purpose.

First let us reset the class to its original definition. (Didn't I tout TclOO's dynamism?)

% oo::define Request { deletemethod Log }
% greystroke head lord.of.the.apes
lord.of.the.apes head

Notice no logging any more. Now we add back logging with the -prepend option.

oo::define Request {
    method Log {args} {
        puts "LOG: [self target]: $args"
        next {*}$args
    }
    filter -prepend Log
}

Now both allowed and denied requests are logged.

% greystroke head lord.of.the.apes
LOG: ::Request head: lord.of.the.apes
lord.of.the.apes head
% carter get princess.of.mars
LOG: ::Request get: princess.of.mars
Go away!

In place of -prepend, you can also specify -remove, -set, -append and -clear with obvious semantics. The last three were already present in Tcl 8.6. Addition of -prepend and -remove simply rounds out the set of operations.

Slots

All this discussion above never mentioned the term slots. Slots generically refer to superclasses, variables, mixins and filters. A class or an object is associated with or contains a list of each of these types. The illustrative example above for filters applies equally to superclasses, variables and mixins as well. Hence the term slot operations. Thus class and object definitions can contain definitions like

mixin -set MixinA MixinB
superclass -append AnotherSuper
variable -clear

and so on.

There is one final point to be noted: if no specific slot operation is specified, the default depends on the slot type. For superclasses and mixins, the default behavior is replacement, i.e. the -set slot operation, while for filters and variables it is -append.

Looking ahead

This post summarized only a few of the enhancements to TclOO in 8.7. In the next post, I will continue with the rest.

References

  1. TIP 500: Private Methods and Variables in TclOO

  2. TIP 519: Inline export/unexport option to TclOO method definition

  3. TIP 516: More OO Slot Operations

  4. oo::define man page