Introducing Tcl 8.7 Part 8: TclOO
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 a pre-alpha binary for your platform. Alternatively, you can build it yourself from the
core-8-branch
branch in the Tcl fossil repository.
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
andinfo 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.