BBS水木清华站∶精华区
发信人: cybergene (基因~也许以后~~), 信区: Linux
标 题: Objects in TCL
发信站: BBS 水木清华站 (Thu Dec 14 16:00:20 2000)
Objects in TCL
------------------------------------------------------------------------
--------
Introduction
C++ and Java are two well-known languages that offer primitives and
in-language support for object oriented programming. That does not
mean that object oriented programming is impossible in other languages.
Object orientation is really just a way of thinking; it has more to
do with design than with implementation. Look at the source code of
the TCL interpreter for a great example of object oriented programming
in C (not C++!). If you are disciplined enough to always pass a
pointer to a struct as the first parameter to a function, you can see
such a function as a 'method' of the struct. You do not need actual
language support to create object-oriented code.
TCL does not offer object oriented primitives, but it is flexible enough
to accomodate new primitives. This paper decribes a well known
technique, object commands, to add some object primitives to TCL. Once
you understand how object commands work, you will be able to figure
out the code of most object packages and extensions.
This paper assumes that you are familiar with TCL, and that you have
written at least a few simple scripts in TCL.
Existing extensions
Many extensions of TCL exist, that add flavours of objects and classes
to the basic language. Some of these extensions are written in C, and
must be compiled and linked to the TCL library to make them available in
the language. A good example is >> [Incr Tcl], an extension that
introduces primitives such as class, method and constructor. Other
extensions, such as Jean-Luc Fontaine's >>Stooop, are written in TCL
itself. They don't require any recompilation. You may wonder how it is
possible to extend TCL with new primitives written in TCL itself. This
paper answers that question by zooming in on the techniques of object
commands and class commands. We will not handle the category of
extensions that requires compilation.
Another important difference is that between extensions with static
and dynamic classes. With static classes, the members of a class
cannot be changed at runtime. You can introduce new classes into a
running system, but once a class is created, you cannot add new
methods or data members to it. Similarly, you can create new instances
of a class, but you cannot (easily) change the class of an existing
instance.
[Incr Tcl] is an example of a TCL extension with static classes. You
cannot add methods or variables to an existing class or object. You can,
however, change the implementation of any method of a class (just
like rewriting a procedure body in pure TCL). And of course, you can
inherit from an existing class and add new methods and variables in
the derived class.
But since TCL is a dynamic language, in which you can introduce new
procedures and new variables at run-time, it seems more appropriate to
also allow the creation of new methods and member variables at run-time.
That requires a dynamic class mechanism such as offered by >> OTcl.
Both static and dynamic object-oriented extensions of TCL can make use
of the techniques described in this paper.
Many thanks for bob Techentin for sharing his [Incr Tcl] knowledge.
A simple example
Suppose we want to manipulate objects that store a single attribute, for
example the name of a color. Each object has its own color. We also
need to give each object a unique identifier or number to distinguish it
from other objects.
We store the object colors in a TCL array, indexed by the object name.
For example:
set a_color(a1) green
set a_color(a2) yellow
set a_color(a3) red
We now have three objects a1, a2, a3, each with its own color. Even an
extremely simple approach like this one is already useful in many
cases where you need to map object attributes to their values.
Simplicity is not a bad property of designs, but a very good one.
We can make this more attractive and hide the array, by writing two
access procedures:
example1/apples.tcl
proc get_color {obj_name} {
global a_color
if { [info exists a_color($obj_name)] } {
return $a_color($obj_name)
} else {
puts "Warning: $obj_name has no color!"
return "transparent" ; # return a default color
}
}
proc set_color {obj_name color} {
global a_color
set a_color($obj_name) $color
}
We now access the colors of objects as follows:
set_color a1 green
puts "a1 has color [get_color a1]"
The next step is to introduce some syntactic sugar: just a small
improvement that makes the syntax look better, but does not really
change anything fundamental. We create the following procedure:
example2/apples.tcl
proc a1 {command args} {
if { $command == "get_color" } {
return [get_color a1]
} elseif { $command == "set_color" } {
set_color a1 [lindex $args 0]
} else {
puts "Error: Unknown command $command"
}
}
Using this procedure, we can now access the color of the a1 object as
follows:
a1 set_color yellow
puts "a1 has color [a1 get_color]"
As you can see, all this really does is swap the positions of the object
name and the name of the get_color or set_color procedure. Not very
useful in itself, but it makes the syntax look more like an object
invocation. It looks as if we invoke the 'method' set_color on the
'object' a1.
Procedure a1 is called an object command. Its first argument is the name
of a method that you want to invoke on the object. The object has the
same name as its object command. Its data is stored in a global array,
in this case a_color, but that is hidden from the programmer by the
object command.
We can now create as many objects as we want: just write a procedure
like a1 for each object, replacing each occurrence of a1 by the name
of another object. Sounds like a lot of work? It is. We will soon see
how you can automate this. Writing a separate procedure for every object
is not only tiresome; it also imposes heavy resource burdens on the
application, because procedures take up space in the TCL interpreter.
The first improvement is that we can write a single dispatcher procedure
like this one:
example3/apples.tcl
proc dispatch {obj_name command args} {
if { $command == "get_color" } {
return [get_color $obj_name]
} elseif { $command == "set_color" } {
set_color $obj_name [lindex $args 0]
} else {
puts "Error: Unknown command $command"
}
}
The object commands can now be written with only a single line of code:
proc a1 {command args} {
return [eval dispatch a1 $command $args]
}
Creating a procedure of this form for each object consumes less memory,
simply because the procedure is shorter. But it is still quite
cumbersome to write a procedure every time you want to instantiate an
object. To simplify this task, we write yet another procedure, one
that creates object commands! It looks like this:
example4/apples.tcl
proc apple {args} {
foreach name $args {
proc $name {command args} \
"return \[eval dispatch $name \$command \$args\]"
}
}
We call this procedure the class command, because it is like a class
type that you can instantiate. Instantiating and manipulating objects is
now as simple as this:
apple a1 a2 a3
a1 set_color green
a2 set_color yellow
a3 set_color red
puts "a1 has color [a1 get_color]"
puts "a2 has color [a2 get_color]"
puts "a3 has color [a3 get_color]"
The class command creates objects of class 'apple'. Each apple has its
own color, which can be accessed through the methods get_color and
set_color of the class.
There are still some pieces missing in the puzzle. First of all, we
now have a way of creating new objects, but we cannot delete objects
yet. This leads to memory leaks, so we need to provide a procedure for
deleting apples:
example5/apples.tcl
proc delete_apple {args} {
global a_color
foreach name $args {
unset a_color($name) ; # Deletes the object's data
rename $name {} ; # Deletes the object command
}
}
We can also set up the array a_color in such a way that $a_color(obj) is
always filled in for every object. We do this in the class command:
proc apple {args} {
foreach name $args {
proc $name {command args} \
"return \[eval dispatch $name \$command \$args\]"
set a_color($name) green
}
}
This makes the class command act like a constructor that sets up the
default values for object attributes. In this case we picked green as
the default color. We now use the complete set of procedures like this:
apple a1 a2 a3
a2 set_color yellow
a3 set_color red
puts "a1 has color [a1 get_color]" ; # Uses default color green
puts "a2 has color [a2 get_color]"
puts "a3 has color [a3 get_color]"
delete a1 a2 a3
Summary
To summarize, we have followed these steps:
Store attributes in a global array
Write a procedure for each 'method' of the object; this method takes the
name of the object as its first argument.
Write a dispatch procedure to call one of those methods.
For each object, write a procedure (object command) with the same name
as the object. Its first argument is the method name. It calls
'dispatch'.
For each class, write a procedure (class command) that creates the
object commands automatically. The class command can also fill in
default attribute values.
For each class, write a delete procedure to reclaim resources of an
object and destroy its object command.
That's it. You now know enough to start using object commands and
class commands in TCL. The rest of this paper offers a few more tips and
tricks, plus (pointers to) real-life examples where object commands are
used.
------------------------------------------------------------------------
--------
More attributes
We will give our apple class some more attributes, to show you how
multiple attributes can be handled. We give each apple a size and a
price (both are integers). These are again stored in global arrays,
for example a_size and a_price. Both are indexed by the name of the
object, just as for the a_color array we've been using so far. And again
we can write get/set procedures to access these new attributes. The
code is very similar to that for the color attribute, so I will not show
it here.
An interesting alternative is to use an array for every object, rather
than an array for every attribute. TCL allows us to create a procedure
and an array variable with the same name, so we can call our object
command 'a1' and use an array 'a1' to store the attributes of that
object. The code of all our procedures now changes slightly:
example6/apples.tcl
proc get_color {obj_name} {
upvar #0 $obj_name arr
return $arr(color)
}
proc set_color {obj_name color} {
upvar #0 $obj_name arr
set arr(color) $color
}
proc dispatch {obj_name command args} {
if { $command == "get_color" } {
return [get_color $obj_name]
} elseif { $command == "set_color" } {
set_color $obj_name [lindex $args 0]
} else {
puts "Error: Unknown command $command"
}
}
proc apple {args} {
foreach name $args {
proc $name {command args} \
"return \[eval dispatch $name \$command \$args\]"
upvar #0 $name arr
set arr(color) green
}
}
proc delete_apple {args} {
foreach name $args {
upvar #0 $name arr
unset arr ; # Deletes the object's data
rename $name {} ; # Deletes the object command
}
}
# Note the advantage of using an array per object:
# 'delete_apple' can just 'unset arr' instead of having to
# remove one entry in three different arrays.
A third alternative is to use only a single, global array, indexed by
the object name and the attribute name. To find the color of apple a1,
you would have to access $attributes(a1,color). The advantage of
having only a single array to maintain, has to be weighed off against
the disadvantage of having to delete several array entries in the
delete_apple procedure.
Configuring the attributes
Another improvement that we can make, is to get rid of all those
annoying get/set methods. We do this by introducing two new methods
for each class, called configure and cget. The first gives new values to
some attributes, the second reads the value of an attribute. We can
implement these procedures for the apple class as follows:
proc dispatch {obj_name command args} {
upvar #0 $obj_name arr
if { $command == "configure" || $command == "config" } {
foreach {opt val} $args {
if { ![regexp {^-(.+)} $opt dummy small_opt] } {
puts "Wrong option name $opt (ignored)"
} else {
set arr($small_opt) $val
}
}
} elseif { $command == "cget" } {
set opt [lindex $args 0]
if { ![regexp {^-(.+)} $opt dummy small_opt] } {
puts "Wrong or missing option name $opt"
return ""
}
return $arr($small_opt)
} elseif { $command == "byte" } {
puts "Taking a byte from apple $obj_name ($arr(size))"
incr arr(size) -1
if { $arr(size) <= 0 } {
puts "Apple $obj_name now completely eaten! Deleting it...
"
delete_apple $obj_name
}
} else {
puts "Error: Unknown command $command"
}
}
# We also change the implementation of the "constructor",
# so that it accepts initializing values for the attributes.
proc apple {name args} {
proc $name {command args} \
"return \[eval dispatch $name \$command \$args\]"
# First set some defaults
upvar #0 $name arr
set arr(color) green
set arr(size) 5
set arr(price) 10
# Then possibly override those defaults with user-supplied
values
if { [llength $args] > 0 } {
eval $name configure $args
}
}
Attribute access now looks exactly as it does for Tk widgets. Compare
these two fragments of code:
button .b -text "Hello" -command "puts world"
.b configure -command "exit"
set textvar [.b cget -text]
apple a -color red -size 5
a configure -size 6
set clr [a cget -color]
Some widget libraries that are written in pure TCL, use object
commands and configure/cget methods to make the widget syntax the same
as in Tk. But obviously, this technique also works for other kinds of
objects.
Object persistence
We will now cover a more exotic topic: object persistence. This means
that you can save an object on disk, and recover it later, in the same
or in another application, even in another process. The recovered object
has exactly the same attributes as the one you saved.
In languages such as C++, object persistence is quite a challenge
(especially if you want to save an object on one platform, and recover
it on another platform with different endianness or with a different
compiler). But the flexibility of TCL makes object persistence a piece
of cake! We will save our objects in a text file, then treat that file
as an Active File to read the objects back (Read more about the Active
File pattern in my paper on TCL file formats, or on >> Nat Pryce's web
site).
We only need a single Tcl procedure (!) to give objects of all classes
the ability to make themselves persistent:
example8/apples.tcl
proc write_objects {classname args} {
foreach name $args {
upvar #0 $name arr
puts "$classname $name \\"
foreach attr [array names arr] {
puts " -$attr $arr($attr) \\"
}
puts ""
}
}
The idea is that this procedure is invoked as follows:
write_objects apple a1 a2 a3
The implementation above shows that the procedure makes the objects a1,
a2, a3 of class 'apple' persistent, by simply outputting a call to
the class command 'apple' followed by the object name and all its
attributes. The resulting output is stored in a file and looks like
this:
apple a1 \
-price 10 \
-size 5 \
-color green \
apple a2 \
-price 10 \
-size 3 \
-color yellow \
apple a3 \
-price 12 \
-size 5 \
-color red \
It is now extremely easy to read these persistent objects back from
disk: just source the file! The source command executes all class
commands in the file, creating instances with exactly the same
attributes as the ones we saved earlier. Object persistence in Tcl is
indeed a piece of cake.
Adding new classes
So far, we have worked with only a single class apple. If we want to add
a new class to our example, we need to write a new class command and
a new dispatcher procedure.
Suppose we also want to have objects of class fridge (in which we will
want to store apples of course). We need to duplicate the effort we
did on the apple class:
example10/classes.tcl
proc dispatch_fridge {obj_name command args} {
upvar #0 $obj_name arr
if { $command == "configure" || $command == "config" } {
array set arr $args
} elseif { $command == "cget" } {
return $arr([lindex $args 0])
} elseif { $command == "open" } {
if { $arr(-state) == "open" } {
puts "Fridge $obj_name already open."
} else {
set arr(-state) "open"
puts "Opening fridge $obj_name..."
}
} elseif { $command == "close" } {
if { $arr(-state) == "closed" } {
puts "Fridge $obj_name already closed."
} else {
set arr(-state) "closed"
puts "Closing fridge $obj_name..."
}
} else {
puts "Error: Unknown command $command"
}
}
proc fridge {name args} {
proc $name {command args} \
"return \[eval dispatch_fridge $name \$command \$args\]"
# First set some defaults
upvar #0 $name arr
array set arr {-state closed -label A}
# Then possibly override those defaults with user-supplied
values
if { [llength $args] > 0 } {
eval $name configure $args
}
}
This laborious task can also be partly automated by a procedure called
class which accepts the name of a new class, a list of its member
variables, and a list of its method names. It then automatically sets up
the necessary procedures such as the class command and the dispatcher
proc. The only thing we still need to implement by hand, are the methods
of the class. The whole thing could be set up as follows:
example11/classes.tcl
proc class {classname vars methods} {
# Create the class command, which will allow new instances to be
created.
proc $classname {obj_name args} "
# The class command in turn creates an object command.
Careful
# with those escape characters!
proc \$obj_name {command args} \
\"return \\\[eval dispatch_$classname \$obj_name \\\$command
\\\$args\\\]\"
# Set variable defaults
upvar #0 \$obj_name arr
array set arr {$vars}
# Then possibly override those defaults with user-supplied
values
if { \[llength \$args\] > 0 } {
eval \$obj_name configure \$args
}
"
# Create the dispatcher, which dispatches to one of the class
methods
proc dispatch_$classname {obj_name command args} "
upvar #0 \$obj_name arr
if { \$command == \"configure\" || \$command == \"config\" }
{
array set arr \$args
} elseif { \$command == \"cget\" } {
return \$arr(\[lindex \$args 0\])
} else {
if { \[lsearch {$methods} \$command\] != -1 } {
uplevel 1 ${classname}_\${command} \$obj_name \$args
} else {
puts \"Error: Unknown command \$command\"
}
}
"
}
The class procedure basically just creates two new commands for us (a
class command and a dispatcher).
The code looks pretty messy, because it contains two levels of
indirection: a proc that creates a proc that creates yet another proc.
This involves a bit of backslash-escape sourcery, which can be
confusing. Richard Suchenwirth has a very nice solution to make this
kind of proc-creating-proc more readable: he creates a template with
names containing a special character such as the '@' sign; then he
replaces those names by the actual class and instance names, using
regsub. See his page on >> gadgets for an example. Using this technique,
our implementation becomes a lot simpler:
example12/classes.tcl
proc class {classname vars methods} {
# Create the class command, which will allow new instances to be
created.
set template {
proc @classname@ {obj_name args} {
# The class command in turn creates an object command.
# Fewer escape characters thanks to the '@' sign.
proc $obj_name {command args} \
"return \[eval dispatch_@classname@ $obj_name \$command
\$args\]"
# Set variable defaults
upvar #0 $obj_name arr
array set arr {@vars@}
# Then possibly override those defaults with user-supplied
values
if { [llength $args] > 0 } {
eval $obj_name configure $args
}
}
}
regsub -all @classname@ $template $classname template
regsub -all @vars@ $template $vars template
eval $template
# Create the dispatcher, which dispatches to one of the class
methods
set template {
proc dispatch_@classname@ {obj_name command args} {
upvar #0 $obj_name arr
if { $command == "configure" || $command == "config" } {
array set arr $args
} elseif { $command == "cget" } {
return $arr([lindex $args 0])
} else {
if { [lsearch {@methods@} $command] != -1 } {
uplevel 1 @classname@_${command} $obj_name $args
} else {
puts "Error: Unknown command $command"
}
}
}
}
regsub -all @classname@ $template $classname template
regsub -all @methods@ $template $methods template
eval $template
}
You see that this simplifies the code. We use the '@' sign because it is
not frequently used in normal TCL code. We postpone the evaluation of
$classname and other variables until we are out of the inner procedure
body, so that the number of escape characters is reduced to almost zero.
With or without this "template" technique, we can now create our
original classes apple and fridge in a more compact way:
example12/classes.tcl
class apple {-color green -size 5 -price 10} {byte}
proc apple_byte {self} {
upvar #0 $self arr
puts "Taking a byte from apple $self"
incr arr(-size) -1
if { $arr(-size) <= 0 } {
puts "Apple $self now completely eaten! Deleting it..."
delete $self
}
}
class fridge {-state closed -label A} {open close}
proc fridge_open {self} {
upvar #0 $self arr
if { $arr(-state) == "open" } {
puts "Fridge $self already open."
} else {
set arr(-state) "open"
puts "Opening fridge $self..."
}
}
proc fridge_close {self} {
upvar #0 $self arr
if { $arr(-state) == "closed" } {
puts "Fridge $self already closed."
} else {
set arr(-state) "closed"
puts "Closing fridge $self..."
}
}
There are several things to note in this implementation:
Creating new classes is indeed a lot simpler than before. We only need
one line with the class "declaration", plus one proc for each of the
class methods.
Each method is implemented as a global proc which has the instance
name as its first argument. Any other arguments are optional.
In the implementation of each method, we access the object's array
directly. We could make the methods less dependent on the actual
implementation of the object by using configure and cget instead, for
example
example13/classes.tcl
proc fridge_close {self} {
if { [$self cget -state] == "closed" } {
puts "Fridge $self already closed."
} else {
$self configure -state "closed"
puts "Closing fridge $self..."
}
}
This is less implementation-dependent, and perhaps slightly more
readable. It is less efficient though, because the configure and cget
implementations add an extra level of procedure calls with a couple of
ifs. You should probably decide for yourself which of the two ways you
are going to use, depending on the importance of efficiency in your
application.
Also note that we can implement the class procedure in a slightly
different way, without actually knowing in advance the list of all the
variables and methods of the class. The new implementation could look
like this:
example14/classes.tcl
# No more 'methods' argument here; 'vars' is optional
proc class {classname {vars ""}} {
# Create the class command, which will allow new instances to
be created.
set template {
proc @classname@ {obj_name args} {
# The class command in turn creates an object command.
# Fewer escape characters thanks to the '@' sign.
proc $obj_name {command args} \
"return \[eval dispatch_@classname@ $obj_name
\$command \$args\]"
# Set variable defaults, if any
upvar #0 $obj_name arr
@set_vars@
# Then possibly override those defaults with
user-supplied values
if { [llength $args] > 0 } {
eval $obj_name configure $args
}
}
}
set set_vars "array set arr {$vars}"
regsub -all @classname@ $template $classname template
if { $vars != "" } {
regsub -all @set_vars@ $template $set_vars template
} else {
regsub -all @set_vars@ $template "" template
}
eval $template
# Create the dispatcher, which does not check what it
# dispatches to.
set template {
proc dispatch_@classname@ {obj_name command args} {
upvar #0 $obj_name arr
if { $command == "configure" || $command == "config" }
{
array set arr $args
} elseif { $command == "cget" } {
return $arr([lindex $args 0])
} else {
uplevel 1 @classname@_${command} $obj_name $args
}
}
}
regsub -all @classname@ $template $classname template
eval $template
}
# ...
fridge f1 -state open
f1 close
# Even after 'f1' is created, we can add a new method to the
'fridge'
# class. 'f1' automatically gets the new method.
proc fridge_paint {self color} {
puts "Painting fridge $self $color ..."
}
f1 paint green
This implementation shows that you can add new methods to an existing
class, simply by implementing a new global procedure named
classname_methodname with self as its first argument. The dispatcher
procedure will find this new method even though it did not yet exist
at the time the class was created. The same is true for member variables
(this has silently been the case in all previous examples, in fact):
just call configure with a new variable name, and it will end up in
the object's array correctly. Only variables specified in the class
procedure get a default value, though; Other variables do not exist
before they are first set by configure!
--
桃花坞里桃花庵,桃花庵下桃花仙;桃花仙人种桃树,又摘桃花卖酒钱。
酒醒只在花前坐,酒醉换来花下眠;半醒半醉日复日,花落花开年复年。
但愿老死花酒间,不愿鞠躬车马前;车尘马足富者趣,酒盏花枝贫者缘。
若将富贵比贫贱,一在平地一在天;若将贫贱比车马,他得驱驰我得闲。
别人笑我忒疯癫,我笑他人看不穿;不见五陵豪杰墓,无花无酒锄做田。
※ 来源:·BBS 水木清华站 smth.org·[FROM: 202.204.7.234]
BBS水木清华站∶精华区