Richard Suchenwirth 2000-11-27 - Things look good. In On things, I have shown a minimal OO API for Tcl, inspired by Self. Here's the initial implementation. See Doing things in namespaces for an advanced version.
Similar to Gadgets, a thing (which can be used like an object, or class... its untypedness fits Tcl pretty well) consists of an array in the caller's scope (may, but need not be global) and a proc, both of same name. The trivial one-liner proc is only needed so the thing can be called by name - it only redirects
proc foo {args} {uplevel thing:dispatch foo $args}
The array holds the properties and the ways (methods) of the thing - all that is to know about the thing. The most important property is the is-a list that every thing has. This lists its superthings and is searched when retrieving a property or way.
The lookup is not transitive, i.e. superthings of superthings are not searched. This is for safety: as hierarchies are not predefined, cycles can easily occur. With a one-pass is-a list, the lookup is guaranteed to terminate. You can have multi-level inheritance if you specify all super*things in the is-a list (OK, not exactly elegant).
Setting properties or ways is always done in the specified thing itself (in contrast to Tcl's namespace resolution, see Dangers of creative writing).
Ways are also in the same array. They are something like procs: they have a name, an arglist (args feature not yet supported) and a body. But using China Blue's procless lambda trick (see Lambda in Tcl), they act like pure lambdas: A list of arglist and body is the value of the (ticked) name and can be assigned to another. To distinguish ways from properties, their name gets a tick prefixed which is stripped off again when listed, so
foo wayto bar {x {expr !$x}} == foo set 'bar {x {expr !$x}}
Now here's the thing:
proc thing {name args} { if {$name=="-names"} {return [thing:names]} if {[info commands $name]!=""} {error "can't make thing $name: exists"} upvar 1 $name self set self(is-a) {} array set self $args regsub @name {uplevel thing:dispatch @name $args} $name body proc $name args $body uplevel trace add variable $name unset thing:unset proc thing:names {} [concat [info body thing:names] $name] set name }
Thing procs are removed when the whole thing (not just an array element) is unset:
proc thing:unset {name el -} { if {$el==""} { rename $name "" set names [info body thing:names] set where [lsearch $names $name] if {$where>0} { proc thing:names {} [lreplace $names $where $where] } } }
The list of things is maintained in a proc body that is extended when a new thing is created, or purged when a thing is deleted:
proc thing:names {} {list}
All thing invocations (the built-ins set, get, unset, is-a, wayto and the ways - I added get as synonym for set, just for sugar) go through this dispatcher:
proc thing:dispatch {name way args} { upvar 1 $name self switch -- $way { get - set { switch -- [llength $args] { 0 { set res [array names self] foreach i $self(is-a) { ladd res [uplevel array names $i] } return $res } 1 { foreach i [concat $name $self(is-a)] { if [llength [uplevel array names $i $args]] { return [uplevel set [set i]($args)] } } } default { array set self $args return [lindex $args end] } } } unset {foreach i $args {unset self($i)}} is-a { if [llength $args] {lappend self(is-a) $args} return $self(is-a) } wayto { switch -- [llength $args] { 0 { set res [list] foreach i [concat $name $self(is-a)] { foreach j [uplevel array names $i '*] { regexp {^'(.+)} $j -> i2 ladd res $i2 } } return $res } 1 {set self('$args)} 2 { foreach {wayname waylambda} $args break set self('$wayname) $waylambda } } } default { foreach i [concat $name $self(is-a)] { if [llength [uplevel array names $i '$way]] { upvar 1 $i super foreach {argl body} $super('$way) break if [llength $argl] { foreach $argl $args break ;# binding to locals } return [eval $body] } } error "$way? Use one of: set, unset, is-a, [uplevel $name wayto]" } } }
Little helper for lappending if the list doesn't have it yet:
proc ladd {_L list} { upvar 1 $_L L foreach i $list { if {[lsearch $L $i]<0} {lappend L $i} } }
That's it. Finally, my little test suite:
###################### Test code set tests { thing human legs 2 thing Socrates is-a {human philosopher} human set mortal 1 Socrates is-a Socrates set hair white Socrates set hair Socrates unset hair Socrates set mortal Socrates set human wayto sing {text {subst $text,$text,lala}} Socrates wayto sing {{text} {subst "$text, haha."}} Socrates wayto sing Socrates wayto Socrates sing Lali [thing Joe is-a human] sing hey thing Plato Plato wayto sing [Socrates wayto sing] Plato sing kalimera catch {Socrates help} res set res thing -names } foreach i [split $tests \n] { puts "$i => [eval $i]" }
General trace handler: I am experimenting with the following handler that will be called on all accesses to a thing:
proc thing:trace {name el mode} { if {$el=="" && $mode=="u"} { rename $name "" set names [info body thing:names] set where [lsearch $names $name] if {$where>0} { proc thing:names {} [lreplace $names $where $where] } } else { upvar 1 $name self if [info exists self('$mode'$el)] { foreach {argl body} $self('$mode'$el) break if [llength $argl] { foreach $argl $args break ;# binding to locals } return [uplevel $body] } } }
It includes the original unset trace for the whole array, but in addition checks whether a way for the operation and element exists, so the test suite can be extended to
Socrates wayto w'legs {{} {puts "hey, I $name have $self(legs) legs"}} Socrates wayto u'legs {{} {puts "hey, I $name need legs"}} Socrates set legs 3 Socrates unset legs
No sugaring for the trace mode letter yet... which is activated by replacing the trace line in proc thing by
uplevel trace add variable $name {read write unset} thing:trace