SS 14 Dec 2004:
It is clear for the Tcl comunity that we need a "standard OOP system" for Tcl.
LV I'd more characterize the situation as that "it is clear that there are those in the Tcl community who desire a single OOP system distributed with the core Tcl distribution". Does that seem a fair rewording of your premise? SS: No ;) If you want, you can write that LV is not included in "we". A fair rewording is that "Most in the Tcl comunity desire ..." IMHO.
But I may be wrong.. so Poll: do you want OOP?.
LV Fair enough - in your opinion, most want this, in my opinion, most do not. Alas, most of the tcl community don't participate in these types of polls, so our opinions will, in all likelihood, remain the same, unless your poll ends up with several hundred thousand respondents...
It is not clear what's the best available, nor if it's wise to get an already developed one, or to create a new one using the experience we collected thanks to the fact that there are a number of OOP systems for Tcl. So while the discussion is hot... I want to express my opinion about it.
I called this object system Odys. The name is related to Dynamic Object System. This are the main ideas:
So, what is lost in power because of semantical simplicity, is gained because all is dynamic. This is the way I think at Tcl itself. Imagine Tcl without introspection, without uplevel, and so on... As a result of this first design principle:
This is the classical toaster example:
class Toaster { var crumbs 0 method toast nslices { if {[$this crumbs] > 50} { error "== FIRE! FIRE! ==" } $this crumbs [expr {[$this crumbs]+4*$nslices}] } method clean {} { $this crumbs 0 } } class SmartToaster extends Toaster { method toast nslices { if {[$this crumbs] > 40} { $this clean } $this parent toast $nslices } }
As you can see:
Instance variables are just methods, if called with a single argument like [$obj foo] the value of the instance variable foo is returned. Instead it can be called with two arguments to set a new value like in [$obj foo $newval]. So instance variables are always accessed using methods, or in other terms, sending messages to the object.
If the object $o is of class xyz that inherits from xy, to call the method foobar of xy with $o as target there is just to write [$o parent foobar]. parent can be used multiple times, like [$o parent parent parent foobar]. Parent is just a method (semantically...), that exists in every class created.
About the fact that it's dynamic. This is what is possible to do:
class foobar { var x 100 classvar y method ciao name {puts "Ciao $name! I'm $this, x = [$this x]"} method hello {} {puts "Hello!"} } set o [new foobar] set class [$o class] puts "The '[$class name]' class methods are: [$class methods]" puts "The '[$class name]' instance vars are: [$class vars]" puts "The '[$class name]' class vars are: [$class classvars]" puts "Method 'ciao' args are: [$class args ciao]" puts "Method 'ciao' body is:\n[$class body ciao]" puts {} puts "Adding a new method to the class..." $class setmethod newmethod {} {puts newmethod} puts "Now the list of methods is: [$class methods]" puts {} puts "Deleting the new method..." $class delmethod newmethod puts "Now the list of methods is: [$class methods]"
Because classes are objects to define a new method there is just to write [$class setmethod foobar ...], and so on.
If you write [new foobar], what you get is something like: ::odysObj::obj0, so to create data structures using objects is simple. This is an example of linked list implementation:
class node { var next {} var value {} } class linkedlist { var head {} method push value { set newnode [new node] $newnode value $value $newnode next [$this head] $this head $newnode return $this } method pop {} { if {[$this head] eq {}} { error "Pop against empty linkedlist object" } set head [$this head] set val [$head value] $this head [$head next] free $head return $val } method len {} { set node [$this head] set len 0 while {$node ne {}} { incr len set node [$node next] } return $len } } set ll [new linkedlist] for {set i 0} {$i < 10} {incr i} { $ll push $i } set node [$ll head] while {$node ne {}} { puts [$node value] set node [$node next] } puts [$ll pop] puts [$ll pop] puts [$ll pop] puts "Len: [$ll len]"
That's all for now. Note that this is just a draft, but I think the main ideas are already present. Every kind of comment, even if very negative, is welcomed.
'''Prototype implementation''' # The ODYS object system # TODO: # # - 'class' class methods to set methods, get methods lists, and # similar things for vars and classvars. # # - Ability to create objects that are auto-destroied when the # procedure where they are created returns. [MSW]: solved by [[local]], # see below. # # - Object to string convertion, and vice versa. # # FEATURES TO REMEMBER: # # - method with empty name is called when the object is # used as a procedure without arguments, like [$myobj]. # For example, in a file object this may return the whole file content. namespace eval odys {} namespace eval odysObj {} set ::odys::objId -1 array set ::odys::class {} array set ::odys::classMethod {} set ::odys::debugFlag 0 proc ::odys::debug str { if {$::odys::debugFlag} { puts $str } } proc new {class args} { set objName "::odysObj::obj[incr ::odys::objId]" # Get the class object if {[catch {set classObj $::odys::class($class)}]} { error "Unknown class '$class'" } namespace eval $objName {} set ${objName}::__class__ $classObj # Copy instance variables into the object set t $classObj while {$classObj ne {}} { foreach var [info vars ${classObj}::vars::*] { set tail [namespace tail $var] set ${objName}::${tail} [set $var] } set classObj [set ${classObj}::parent] } # Create the alias interp alias {} $objName {} ::odys::dispatch $objName uplevel 1 $objName init $args return $objName } proc local {class args} { set obj [uplevel new $class $args] set fobj [string map {: _} $obj] uplevel set $fobj 1 uplevel trace add variable $fobj unset cleanObj return $obj } proc cleanObj {obj args} { set obj [string map {_ :} $obj] free $obj } proc ::odys::dispatch {id args} { ::odys::debug "Object $id received message '$args'" set classObj [set ${id}::__class__] # Handle 'parent' special method while {[lindex $args 0] eq {parent}} { if {[set ${classObj}::parent] eq {}} { error "Object's class has not parent." } set classObj [set ${classObj}::parent] set args [lrange $args 1 end] } # Method lookup while 1 { if {[info proc ${classObj}::methods::[lindex $args 0]] ne {}} { return [uplevel 1 ${classObj}::methods::[lindex $args 0] $id \ [lrange $args 1 end]] } if {[set ${classObj}::parent] eq {}} { error "No such method '$method'" } # Retry with the parent set classObj [set ${classObj}::parent] } } set ::odys::class(class) {} proc free id { $id free namespace delete $id interp alias {} $id {} } proc class {name args} { switch -- [llength $args] { 1 {set parentClassObj {}; set body [lindex $args 0]} 3 { if {[lindex $args 0] ne {extends}} { error {wrong args: second argument must be "extends" if present} } set parent [lindex $args 1]; set body [lindex $args 2] if {[catch {set parentClassObj $::odys::class($parent)}]} { error "Unknown class '$class'" } } default { error {wrong # args: should be "class className ?extends parentName? body"} } } set classObj [new class] namespace eval ${classObj}::methods {} namespace eval ${classObj}::vars {} namespace eval ${classObj}::classvars {} interp alias {} ${classObj}::method {} ::odys::method interp alias {} ${classObj}::var {} ::odys::var interp alias {} ${classObj}::classvar {} ::odys::classvar proc ${classObj}::methods::init this {} proc ${classObj}::methods::free this {} proc ${classObj}::methods::class this { set ${this}::__class__ } set ${classObj}::name $name if {[catch {namespace eval $classObj $body} errmsg]} { free $classObj error $errmsg } set ${classObj}::parent $parentClassObj set ::odys::class($name) $classObj } proc ::odys::method {name arglist body} { set classObj [uplevel namespace current] proc ${classObj}::methods::${name} [concat this $arglist] $body } proc ::odys::var {name {value {}}} { set classObj [uplevel namespace current] proc ${classObj}::methods::${name} {this args} [format { if {[llength $args] == 0} { return [set ${this}::%s] } elseif {[llength $args] == 1} { return [set ${this}::%s [lindex $args 0]] } else { error {instance variable access method can accept 0 or 1 arg.} } } $name $name] set ${classObj}::vars::${name} $value } proc ::odys::classvar {name {value {}}} { set classObj [uplevel namespace current] proc ${classObj}::methods::${name} {this args} [format { if {[llength $args] == 0} { return [set [set ${this}::__class__]::classvars::%s] } elseif {[llength $args] == 1} { return [set [set ${this}::__class__]::classvars::%s [lindex $args 0]] } else { error {class variable access method can accept 0 or 1 arg.} } } $name $name] set ${classObj}::classvars::${name} $value } proc ::odys::isClassObj id { if {![namespace exists $id]} {return 0} if {[$id class] ne $::odys::classClassObj} {return 0} return 1 } ############################### The 'class' class ############################## # This class is build by hand because it's somewhat special, and # the [class] command itself requires this class to be already-working. # Create the 'class' class. namespace eval ::odysObj::obj0 {set parent {}} namespace eval ::odysObj::obj0::methods {} proc ::odysObj::obj0::methods::init this {} set ::odys::class(class) ::odysObj::obj0 set ::odys::classClassObj [new class] # Initialize this class by hand. namespace eval ${::odys::classClassObj}::methods {} namespace eval ${::odys::classClassObj}::vars {} namespace eval ${::odys::classClassObj}::classvars {} set ${::odys::classClassObj}::vars::methods {} set ${::odys::classClassObj}::vars::vars {} set ${::odys::classClassObj}::vars::classvars {} set ${::odys::classClassObj}::parent {} set ${::odys::classClassObj}::__class__ $::odys::classClassObj proc ${::odys::classClassObj}::methods::free {this} {} proc ${::odys::classClassObj}::methods::name {this} {set ${this}::name} proc ${::odys::classClassObj}::methods::setparent {this args} { if {[llength $args] == 1} { set newparent [lindex $args 0] if {$newparent ne {} && ![::odys::isClassObj $newparent]} { error "Can't set '$newparent' as parent. Not a class object." } set ${this}::parent $newparent } elseif {[llength $args] == 0} { set ${this}::parent } else { error "bad # of args: setparent can accept 0 or 1 argument." } } proc ${::odys::classClassObj}::methods::methods this { set methods [info procs ${this}::methods::*] set tails {} foreach m $methods { lappend tails [namespace tail $m] } return $tails } proc ${::odys::classClassObj}::methods::vars this { set vars [info vars ${this}::vars::*] set tails {} foreach v $vars { lappend tails [namespace tail $v] } return $tails } proc ${::odys::classClassObj}::methods::classvars this { set vars [info vars ${this}::classvars::*] set tails {} foreach v $vars { lappend tails [namespace tail $v] } return $tails } proc ${::odys::classClassObj}::methods::args {this name} { lrange [info args ${this}::methods::${name}] 1 end } proc ${::odys::classClassObj}::methods::body {this name} { info body ${this}::methods::${name} } proc ${::odys::classClassObj}::methods::setmethod {this name args body} { set args [concat this $args] proc ${this}::methods::${name} $args $body } proc ${::odys::classClassObj}::methods::delmethod {this name} { rename ${this}::methods::${name} {} } set ::odys::class(class) ${::odys::classClassObj} ################################################################################
Some more example of usage
This is a file object:
source odys.tcl class file { var fd {} method init args { if {[llength $args] == 0} return eval $this open $args } method free {} { set fd [$this fd] if {$fd ne {}} { ::close $fd } } method open args { $this fd [eval ::open $args] } method close {} { ::close [$this fd] $this fd {} } method gets args { switch [llength $args] { 0 {::gets [$this fd]} 1 { upvar 1 [lindex $args 0] line ::gets [$this fd] line } default { error "wrong # of args, try: gets ?varname?" } } } method rewind {} { seek [$this fd] 0 } method foreach {varname script} { set fd [$this fd] upvar $varname line while {[::gets $fd line] != -1} { uplevel 1 $script } } method {} {} { $this rewind set buf [read [$this fd]] $this rewind return $buf } } set t [new file /etc/passwd] $t foreach line {puts -nonewline "[string length $line] "} puts {} $t close free $t set t [new file /etc/resolv.conf] puts [$t] free $t '''Class vars example''' source odys.tcl class foobar { classvar x 0 } set a [new foobar] set b [new foobar] $a x 100 puts [$a x] puts [$b x] '''Auto clean example''' source odys.tcl class tst { var msg {} method init args { $this msg $args } method free {} { puts "Freeing [$this msg]" } } proc a {} { set t1 [new tst "Test #1"] set t2 [local tst "Test #2"] } a
Comments
WHD: Implementing instance variables as methods is interesting, but ultimately frustrating; you lose incr, lappend, etc. You need to have instance variables that can be handed to another object and traced, e.g., as -textvariables. Also, you're going to need array variables. DKF has done some interesting stuff using Tcl 8.5 dicts to store instance variables; the dict with command brings all of the dict fields into scope as variables, and packs any changes back into the dict.
SS: Hello! I'm not sure how this will evolve, but as you suggest it's very likely that Odys will be implemented using dict and not namespaces because a derivated of Odys is going to be the default OOP system of Jim that has no namespaces but supports dicts. About instance variables as methods, I've the idea to fix the problem about the fact that they don't combine very well with commands accepting var names as arguments using a with similar to dict itself, i.e.:
$obj with x {incr x}
The with method will care to map 'x' to the value returned by the method, and then set it back to the object, not only, if 'x' already exists tehre will be no collision, the 'x' is saved (or is saved the fact that it didn't existed), and then it's restored with the old content or destroied at all. This should make Odys able to take the "the only interface to objects is to send they messages", without to make it a pain to use. Thank you very much for your comment.
WHD: "$obj with x" is an interesting thought; would the "with" method be available to outside users, or would it work only within methods? And if you do this, you'd best make it work with lists of instance variables, e.g., "$obj with {x y} ..." On the other hand, if you store instance variables as dicts you can make them transparently available in your methods, which would be nicer.
SS: yep, "with" should be available to outside users, another design principle of Odys is that there is no method privare, nor instance variable public, while a derived class may decide to override a parent's method with an [error "you can't call this"]. Also it seems to me a good idea to allow lists of instance variables, and I may even support something like: [$obj with {x y} as {a b} { .... }] that I think will be rarely used, but may be a good way to deal with cases where there is collision between object's instance variables names and variables defined in the current context that you want to use inside the "with" script. Even if I implement Odys with dict, still I think that can be nice to allow to access the object only via methods: the derived class can override methods and instance variables in the same way, also to support polymorphism seems simpler this way. My only support for instance variables is in the class definition where it's nice if Odys will auto-create the default method to set/get the variable. Performances should not be a big problem since all this will be implemented in C inside Jim... but there is to test to really check I'll be able to reach usable-level performances.
WHD: Override instance variables in subclasses? That sounds like a quick trip to hell, to me. In fact, the more I think about having all of your instance variables be publicly available as methods, the less I like it. Part of good design is protecting your object's invariants. You can't protect them if every instance variable is accessible to all comers. At the very, very least you should adopt a naming convention where (say) capitalized methods and variables are intended for use internally, while lowercase methods and variables are for external use. It doesn't need to be enforced by Odys, but it'll help make it clear to the reader just what the real interface is.
SS: you are right, I didn't considered this well enough. The design I want to reach is that the only way to use an object is by methods, i.e. there must be no concept of accessing an object's instance variable directly like C++'s obj.c = 5. I may like to apply the same concept even in derivated classes, that should be able to use the inherithed class only via the exported interface. This basically means no explicit control of what is public/private. All methods public, all instance variables private, and the class will export when appropriate methods to allow modification of the object's instance variables (and generally, state). In order to reach this, is probaby wise to implement instance variables not as methods, but as you suggested like normal-looking variables. To make simpler to build methods that have just the semantic of [$obj x] to return $x, and [$obj x $newVal] to set 'x' to the new value, maybe I can allow to write:
class foobar { set x 0 varmethod x }
instead of
class foobar { set x 0 method x {args} { if [llength $args] .... } }
In this new shape, does this OOP system sound more sane? My priority is to make it usable, class-based, dynamic, and with very few concepts. Thanks for your help. Salvatore.
WHD: Much more sane. In my experience, "public methods, private variables" is almost always right. There are rare occasions, as I say, when you want to give an external entity access to a variable, e.g., as a label widget's -textvariable; as DKF has shown, though, you can do that even if your instance variables aren't real Tcl variables by creating a Tcl variable and a read trace when you need one.
SS: Ok, in Jim there are no variable traces... I don't want to add traces, but other guys involved in the project may like they. I'm pondering... btw I think for now I'll ignore this rare cases where to export an instance var is needed ;). I'll update this page when the OOP extension of Jim (following this guidelines) is ready. Thanks.