Version 23 of TclOO Tricks

Updated 2010-02-24 23:41:18 by mpdanielson

TclOO appears to be a fairly bare-bones OO system, but there are many thing that it can do with just a little bit of scripting. Here are a few...


Class Variables

oo::class create foo {
   self export varname
   constructor {} {my eval upvar [[self class] varname v] v}
   method bar {} {variable v; incr v}
}

Demoing it...

% foo create x
::x
% x bar
1
% x bar
2
% foo create y
::y
% y bar
3

TJE: Here's the same, re-factored as a mixin for easier re-use. Using "my static" instead of "my variable" inside any method exposes the named static variable(s).

oo::class create Static {
    method static {args} {
        if {![llength $args]} return
        set callclass [lindex [self caller] 0]
        define $callclass self export varname
        foreach vname $args {
            lappend pairs [$callclass varname $vname] $vname
        }
        uplevel 1 upvar {*}$pairs
    }
}

Demoing it...

oo::class create Foo {
    mixin Static
    variable _inst_num

    constructor {} {
        my static _max_inst_num
        set _inst_num [incr _max_inst_num]
    }

    method inst {} {
        return $_inst_num
    }
}

% [Foo new] inst
1

% [Foo new] inst
2

DKF: Here's how to do it by leveraging the scripted guts of TclOO (from my Tcl2k9 paper):

proc ::oo::Helpers::classvar {name args} {
    # Get reference to class’s namespace
    set ns [info object namespace [uplevel 1 {self class}]]

    # Double up the list of varnames
    set vs [list $name $name]
    foreach v $args {lappend vs $v $v}

    # Link the caller’s locals to the
    # class’s variables
    tailcall namespace upvar $ns {*}$vs
}

Demonstrating:

% oo::class create Foo {
    method bar {z} {
        classvar x y
        return [incr x $z],[incr y]
    }
}
::Foo
% Foo create a
::a
% Foo create b
::b
% a bar 2
2,1
% a bar 3
5,2
% b bar 7
12,3
% b bar -1
11,4
% a bar 0
11,5

If you don't have tailcall, use this instead inside the definition:

 uplevel 1 [list namespace upvar $ns {*}$ns]

Class (Static) Methods

proc ::oo::define::classmethod {name {args ""} {body ""}} {
    # Create the method on the class if
    # the caller gave arguments and body
    set argc [llength [info level 0]]
    if {$argc == 4} {
        uplevel 1 [list self method $name $args $body]
    } elseif {$argc == 3} {
        return -code error "wrong # args: should be \"[lindex [info level 0] 0] name ?args body?\""
    }

    # Get the name of the current class
    set cls [lindex [info level -1] 1]

    # Get its private “my” command
    set my [info object namespace $cls]::my

    # Make the connection by forwarding
    tailcall forward $name  $my $name
}

Usage:

oo::class create Foo {
    classmethod boo {x} {
        puts "This is [self]::boo with argument x=$x"
    }
}

Foo create bar
bar boo 42
# --> This is ::Foo::boo with argument x=42
Foo boo 7
# --> This is ::Foo::boo with argument x=7

If you don't have tailcall, use this instead inside the definition:

 uplevel 1 [list forward $name $my $name]

dzach: So, if I get it correctly, to create a class (static) method as shown above, one creates the method on the object of the class (i.e. an object method) and then creates a forward to it (i.e. a class forward), which (class forward) is then accessible by the class instances. Maybe the first comment line should read "# create the method on the class object if"


Singletons

oo::class create singleton {
   superclass oo::class
   variable object
   method create {name args} {
      if {![info exists object]} {
         set object [next $name {*}$args]
      }
      return $object
   }
   method new args {
      if {![info exists object]} {
         set object [next {*}$args]
      }
      return $object
   }
}

Demoing...

% oo::class create example {
   self mixin singleton
   method foo {} {self}
}
::example
% [example new] foo
::oo::Obj22
% [example new] foo
::oo::Obj22

Private keyword for methods (working)

FF: I ended up in this solution for having private working both inside oo::class create xxx and in oo::define xxx {..}.

proc oo::define::private {what methodName argList body} {
    set fn [info frame]
    while {$fn > 0} {
        set cmd [dict get [info frame $fn] cmd]
        switch -- [lindex $cmd 0] {
        oo::define { set c [lindex $cmd 1]; set fn 0 }
        oo::class { set c [lindex $cmd 2]; set fn 0 }
        }
        incr fn -1
    }
    ::oo::define $c [list $what $methodName $argList $body]
    ::oo::define $c [list unexport $methodName]
}

Method visibility keyword (private) (2)

oo::objdefine oo::class method private {method methodName argList body} {
    if {$method != "method"} {return -code error "must be: private method <name> <args> <body>"}
    my method $methodName $argList $body
    my unexport $methodName
}
oo::class create test {
    private method foo {} {puts "foo"}
    method bar {} {puts "bar"; my foo}
}

but doesn't work (invalid command name "private"). why?

DKF: This isn't documented (or supported, so liable to change without warning: caveat emptor) but you do this by creating commands in the namespace ‘::oo::define’ and ‘::oo::objdefine’. For example:

proc oo::define::private {method methodName argList body} {
    if {$method ne "method"} {return -code error "must be: private method <name> <args> <body>"}
    uplevel 1 [list method $methodName $argList $body]
    uplevel 1 [list unexport $methodName]
}

Much of TclOO's internal workings are really just simple reusing of existing Tcl things, but with interesting twists.

FF I tried it with Tcl-8.5 and TclOO-0.6 but get this error:

this command may only be called from within the context of an ::oo::define or ::oo::objdefine command
    while executing
"method $methodName $argList $body"
    (procedure "private" line 3)

It seems proc oo::define::private {} {..} doesn't know the context (that is: the classname) the function gets called

DKF: Oops, forgot that. Adjusted to add uplevels where needed...

FF: This method does not seem to work. Try the previous one in case.

DKF: Turns out there was a bug (fixed in the HEAD of Tcl and TclOO-standalone) where it was looking for the context object in the wrong location.


Friendly Objects

DKF: … or how to let one object invoke private methods on another …

# This is a normal class except for the accessor method.
oo::class create foo {
    variable x
    constructor {} {
        set x 1
    }
    method next {} {
        incr x
        puts $x
    }
    method BOOM {} {puts "Boom boom!"}
    method accessor {} {namespace which my}
}
# This class will make use of the accessor method to connect
oo::class create bar {
    variable y
    constructor {other} {
        # Allow access to other object's private methods through the local 'other' command
        interp alias {} [self namespace]::other {} [$other accessor]
        # Allow access to other object's variables, now we have the accessor mapped
        my eval [list upvar 0 [other varname x] y]
    }
    forward boom  other BOOM
    method reset {} {
        set y 0
        puts resetting...
    }
}

# Make the instances...
foo create fooInstance
bar create barInstance fooInstance

# Call a private method indirectly
barInstance boom

# Demonstrate that there is a single shared variable
fooInstance next
fooInstance next
barInstance reset
fooInstance next

(Requires the CVS HEAD as I write this in order to fix a bug with forward resolution scope...)

The above prints this, with no errors:

Boom boom!
2
3
resetting...
1

Appending Variable Declaration

DKF: You can script in a nicer way of declaring variables:

proc oo::define::Variable args {
    set currentclass [lindex [info level 1] 1]
    set existing [uplevel 1 [list info class variables $currentclass]]
    switch [lindex $args 0] {
        -append {
            set vars $existing
            lappend vars {*}[lrange $args 1 end]
        }
        -prepend {
            set vars [lrange $args 1 end]
            lappend vars {*}$existing
        }
        -remove {
            set vars $existing
            foreach var [lrange $args 1 end] {
                set idx [lsearch -exact $vars $var]
                if {$idx >= 0} {
                    set vars [lreplace $vars $idx $idx]
                }
            }
        }
        -set - default {
            set vars [lrange $args 1 end]
        }
    }
    uplevel 1 [list variables {*}$vars]
    return
}

Which then lets you do this:

oo::class create foo {
    Variable x y
    Variable -append p d q
    method bar args {
        lassign $args x y p d q
    }
    method boo {} {
        return $x,$y|$p,$d,$q
    }
}

Note that this only works reliably for class declarations. (Well, that's all it works for syntactically anyway, but the obvious adaptations don't work reliably.) This is because of self declarations; there really ought to be a better mechanism for determining the current class or object in a declaration context...


Pool Allocator

From Rosetta Code :

package require Tcl 8.6
oo::class create Pool {
    superclass oo::class
    variable capacity pool busy
    unexport create
    constructor args {
        next {*}$args
        set capacity 100
        set pool [set busy {}]
    }
    method new {args} {
        if {[llength $pool]} {
            set pool [lassign $pool obj]
        } else {
            if {[llength $busy] >= $capacity} {
                throw {POOL CAPACITY} "exceeded capacity: $capacity"
            }
            set obj [next]
            set newobj [namespace current]::[namespace tail $obj]
            rename $obj $newobj
            set obj $newobj
        }
        try {
            [info object namespace $obj]::my Init {*}$args
        } on error {msg opt} {
            lappend pool $obj
            return -options $opt $msg
        }
        lappend busy $obj
        return $obj
    }
    method ReturnToPool obj {
        try {
            if {"Finalize" in [info object methods $obj -all -private]} {
                [info object namespace $obj]::my Finalize
            }
        } on error {msg opt} {
            after 0 [list return -options $opt $msg]
            return false
        }
        set idx [lsearch -exact $busy $obj]
        set busy [lreplace $busy $idx $idx]
        if {[llength $pool] + [llength $busy] + 1 <= $capacity} {
            lappend pool $obj
            return true
        } else {
            return false
        }
    }
    method capacity {{value {}}} {
        if {[llength [info level 0]] == 3} {
            if {$value < $capacity} {
                while {[llength $pool] > 0 && [llength $pool] + [llength $busy] > $value} {
                    set pool [lassign $pool obj]
                    rename $obj {}
                }
            }
            set capacity [expr {$value >> 0}]
        } else {
            return $capacity
        }
    }
    method clearPool {} {
        foreach obj $busy {
            $obj destroy
        }
    }
    method destroy {} {
        my clearPool
        next
    }
    self method create {class {definition {}}} {
        set cls [next $class $definition]
        oo::define $cls method destroy {} {
            if {![[info object namespace [self class]]::my ReturnToPool [self]]} {
                next
            }
        }
        return $cls
    }
}

Easier Callbacks

It's nice to use a private method for callbacks (for fileevent, trace, bind, etc.) This makes that easier:

proc ::oo::Helpers::callback {method args} {
    list [uplevel 1 {namespace which my}] $method {*}$args
}

Usage:

oo::class create CopyToStdout {
    variable f
    constructor {fd} {
        set f $fd
        fileevent $fd readable [callback Readable]
    }
    method Readable {} {
        if {[gets $f line] >= 0} {
            puts $line
        } else {
            my destroy
        }
    }
    destructor {
        close $f
    }
}
new CopyToStdout [socket example.com 12345]

Ensemble Methods or SNIT-like Methods

mpdanielson: Borrowing from classmethod above, this proc creates ensemble methods that almost act like real methods. "self" and "my" don't work, but uplevel can be used to get back into the calling object's context.

proc ::oo::define::ensemble { name argList bodyScript } {
    if { [llength $name] == 1 } {
        tailcall method $name $argList $bodyScript
    }
    set cls [info object namespace [lindex [info level -1] 1]]
    set ns  ${cls}::[join [lrange $name 0 end-1] ::]
    set cmd [lindex $name end]
    namespace eval $ns [list proc $cmd $argList $bodyScript]
    for {set i 1} {$i<[llength $name]} {incr i} {
        namespace eval $ns [list namespace export $cmd]
        namespace eval $ns {namespace ensemble create}
        set cmd [namespace tail $ns]
        set ns  [namespace qualifiers $ns]
    }
    set entry [lindex $name 0]
    tailcall method $entry { args } [format {
        return [namespace eval %s [list %s {*}$args]]
    } $cls $entry]
} ;# proc ::oo::define::ensemble

Usage:

oo::class create Test {
    ensemble {e0 add} { a b } {
        return "$a + $b = [expr {$a + $b}]"
    }
    ensemble {e0 mul} { a b } {
        return "$a * $b = [expr {$a * $b}]"
    }
    ensemble {e0 caller} {} {
        return "called from [uplevel 2 self]"
    }
    ensemble {e0 x mod} {a b} {
        return "$a % $b = [expr {$a % $b}]"
    }
    ensemble {e1 sub} { a b } {
        return "$a - $b = [expr {$a - $b}]"
    }
    ensemble {e1 div} { a b } {
        return "$a / $b = [expr {$a / $b}]"
    }
    ensemble m0 {} {
        return "plain method"
    }
} ;# class Test

(menu) 1 % Test create t
::t
(menu) 2 % t e0 add 3 4
3 + 4 = 7
(menu) 3 % t e0 caller
called from ::t
(menu) 4 % t e0 x mod 7 3
7 % 3 = 1