Playing with TclOO

HolgerJ 2015-06-25 - I was wondering whether objects can go out of scope and get deleted like they do in C++ (whenever the block ends) or in Java (when the reference count allows the garbage collection to remove it). At #EuroTcl2015 in Cologne we discussed possibilities, although objects in TclOO are commands and therefore are not tied to the block (or proc) where they have been created. I wrote about it on the page object with scope

Richard Suchenwirth 2007-10-20 - Now that I have TclOO both at work and home, and another weekend comes up, it's obvious that I wanted to play with it, just for finger practice. Barking dogs have been demonstrated elsewhere, but I decided to start with another classic example, a bank account.

 #!/usr/bin/env tclsh85
 package require TclOO
 namespace import oo::*
 catch {Account destroy} ;# in case of repeated sourcing

 class create Account {
    constructor {{ownerName undisclosed}} {
        my variable total overdrawLimit owner
        set total 0
        set overdrawLimit 10
        set owner $ownerName 
    }
    method deposit amount {
        my variable total
        set total [expr {$total + $amount}]
    }
    method withdraw amount {
        my variable {*}[info object vars [self]] ;# "auto-import" all variables
        if {($amount - $total) > $overdrawLimit} {
            error "Can't overdraw - total: $total, limit: $overdrawLimit"
        }
        set total [expr {$total - $amount}]
    }
    method transfer {amount targetAccount} {
        my variable total
        my withdraw $amount
        $targetAccount deposit $amount
        set total
    }
    destructor {
        my variable total
        if {$total} {puts "remaining $total will be given to charity"}
    }
 }

Due to the simple hierarchy of TclOO (every class is an instance of oo::class, which is an subclass of oo::object - while at the same time oo::object is an instance of oo::class), it is not very hard to add development helper methods that each object inherits. For instance, this dumps an object's class and instance variables into a dict-able list:

 define oo::object method dump {{pat *}} {
    set res [list class [info object class [self]]]
    foreach i [info object vars [self] $pat] {
        my variable $i
        lappend res $i [set $i]
    }
    set res
 }

see also Serializing TclOO objects

#-- "Test suite"

 foreach cmd [split {
    set a [Account new "John Doe"]
    $a deposit 200
    $a deposit 20
    $a withdraw 150
    $a withdraw 100
    $a dump
    set b [Account new]
    $a transfer 65 $b
    $a dump
    $b dump
    $a transfer 1000000 $b
    $b destroy
 } \n] {
    catch $cmd res
    puts "$cmd -> $res"
 }

which prints

    set a [Account new "John Doe"] -> ::oo::Obj36
    $a deposit 200 -> 200
    $a deposit 20 -> 220
    $a withdraw 150 -> 70
    $a withdraw 100 -> Can't overdraw - total: 70, limit: 10
    $a dump -> class ::Account total 70 overdrawLimit 10 owner {John Doe}
    set b [Account new] -> ::oo::Obj37
    $a transfer 65 $b -> 5
    $a dump -> class ::Account total 5 overdrawLimit 10 owner {John Doe}
    $b dump -> class ::Account total 65 overdrawLimit 10 owner undisclosed
    $a transfer 1000000 $b -> Can't overdraw - total: 5, limit: 10
 remaining 65 will be given to charity
    $b destroy ->

Interactive testing is, as usual with Tcl, a good way to learn. In the following case, it shows that variables are not exactly protected from hack attacks:

 % set $a\::total 1000000
 1000000
 % $a dump
 class ::Account total 1000000 overdrawLimit 10 owner {John Doe}

So better not deploy a real banking software based on this code :^) On the lighter side, introspection works as we all know and love, for easy "online help":

 % class help
 unknown method "help": must be create, destroy or dump
 % Account help
 unknown method "help": must be create, destroy, dump or new
 % $a help
 unknown method "help": must be deposit, destroy, dump, transfer or withdraw

Another issue: member variables must be explicitly imported into methods (similar to namespace rules). If you prefer the C++ style (all member variables implicitly known), one hackish solution was demonstrated in the withdraw method above, and here's some sugar-coating if you want it more often:

 proc all_my_variables {} {
    uplevel 1 {my variable {*}[info object vars [self]]}
 }

so the method can then look like

    method withdraw amount {
        all_my_variables
        if {($amount - $total) > $overdrawLimit} {
            error "Can't overdraw - total: $total, limit: $overdrawLimit"
        }
        set total [expr {$total - $amount}]
    }

To distinguish members and others, one might then use the C++ convention that member names end in "_".

To summarize my first impressions, TclOO can be used well as an OO system in itself. I haven't even used all features as documented in TIP #257. So when I'll need some OO in the future, I can probably just use this, and do without incr Tcl or XOTcl ... :^)


The most part of the above example was also included in http://en.wikibooks.org/wiki/Tcl_Programming/Introduction#TclOO - everyone is welcome to make it better!


DKF: Well, you can go one better:

 oo::class create class {
    superclass oo::class
    constructor {args} {
       my variable vars
       set vars {}
       next {*}$args
    }
    method global {variable {initValue {}}} {
       my variable vars
       lappend vars $variable $initValue
    }
    method constructor {args body} {
       my variable vars
       set body0 {}
       foreach {var val} $vars {append body0 [list variable $var $val] \;}
       oo::define [self] constructor $args $body0$body
    }
    method method {name args body} {
       my variable vars
       set body0 {}
       foreach {var val} $vars {append body0 [list variable $var] \;}
       oo::define [self] method $name $args $body0$body
    }
    method destructor body {
       my variable vars
       set body0 {}
       foreach {var val} $vars {append body0 [list variable $var] \;}
       oo::define [self] destructor $body0$body
    }
 }

 class create Account
 Account global total 0
 Account global overdrawLimit 10
 Account global owner
 Account constructor {{ownerName undisclosed}} {
    set owner $ownerName 
 }
 Account method deposit amount {
    set total [expr {$total + $amount}]
 }
 Account method withdraw amount {
    if {($amount - $total) > $overdrawLimit} {
       error "Can't overdraw - total: $total, limit: $overdrawLimit"
    }
    set total [expr {$total - $amount}]
 }
 Account method transfer {amount targetAccount} {
    my withdraw $amount
    $targetAccount deposit $amount
    return $total
 }
 Account destructor {
    if {$total} {puts "remaining $total will be given to charity"}
 }

DKF: And then there's another step beyond...

 oo::class create globalizedClass {
    superclass oo::class
    constructor {args} {
       my variable vars
       set vars {}
       foreach m {global constructor method destructor} {
          interp alias {} [self namespace]::$m {} [self] $m
       }
       my eval {*}$args
    }
    method global {variable {initValue {}}} {
       my variable vars
       lappend vars $variable $initValue
    }
    method constructor {args body} {
       my variable vars
       set body0 variable
       foreach {var val} $vars {lappend body0 $var $val}
       oo::define [self] constructor $args $body0\;$body
    }
    method method {name args body} {
       my variable vars
       set body0 {my variable}
       foreach {var val} $vars {lappend body0 $var}
       oo::define [self] method $name $args $body0\;$body
    }
    method destructor body {
       my variable vars
       set body0 {my variable}
       foreach {var val} $vars {lappend body0 $var}
       oo::define [self] destructor $body0\;$body
    }
 }

 globalizedClass create Account {
    global total 0
    global overdrawLimit 10
    global owner
    constructor {{ownerName undisclosed}} {
       set owner $ownerName
    }
    method deposit amount {
       set total [expr {$total + $amount}]
    }
    method withdraw amount {
       if {($amount - $total) > $overdrawLimit} {
          error "Can't overdraw - total: $total, limit: $overdrawLimit"
       }
       set total [expr {$total - $amount}]
    }
    method transfer {amount targetAccount} {
       my withdraw $amount
       $targetAccount deposit $amount
       return $total
    }
    destructor {
       if {$total} {puts "remaining $total will be given to charity"}
    }
 }

It's because it can support this sort of thing that I like to think of TclOO as being a kit for building OO systems.


SL: Another interesting customizing on the meta-class oo::class inspired by Putting Metaclasses to Work [Ira R. Forman, Scott H. Danforth]:

Each newly created oo::object should contain a timestamp, holding the creation-time of each instance.

One implementation is called instance method:

 oo::class create ctimestamped {
     method create {args} {        
         next {*}$args
         oo::define [lindex $args 0] constructor {args} {
             my variable timestamp
             set timestamp [clock seconds]
             next {*}$args
         }
         oo::define [lindex $args 0] method getCreationTime {} {
             my variable timestamp
             return $timestamp
         }
     }
 }
 oo::define oo::class mixin ctimestamped
 ...
 set a [Account new]
 $a getCreationTime

The timestamp is located in the class instance as well as the retrieving function.

Another implementation is called class method:

 oo::class create ctimestamped {
     method create {args} {
         next {*}$args
         oo::define [lindex $args 0] constructor {args} {
             my variable timestamp
             set timestamp [clock seconds]
             next {*}$args
         }
     }
     method getCreationTime object {
         set ${object}::timestamp
     }
 }
 oo::define oo::class mixin ctimestamped
 ...
 set a [Account new]
 Account getCreationTime $a

The timestamp is located in the class instance, but the retrieving function is located in the meta-class oo::class.


[tb] 2008-09-24 - How about cascading message posts?

 set aLine [[[MyString appendString AnotherString] append "\n"] printString] 

isn't quite comfortable, isn't it?

DKF: I can't say that I'm too keen on that sort of usage either, but then again I'd just point out that that's using a perfectly Tcl-ish system in a non-Tcl-ish way. As such, "not a real problem". (I'd instead have the append method take a variable number of arguments and return the resulting string. Which *is* Tcl-ish…)

tb: Ahh... - yes, I see. It'd be nice to have, but it wouldn't be Tcl-ish enough to integrate well. TclTk isn't Smalltalk :)

Lars H: While I don't see much point in this construction either, I'd like to remark that the TIP#314 [1 ] mechanism supports it. After

namespace eval String {
    # The namespace ensemble is a "class". Its "objects" are command prefixes
    # starting with the command My, which is the dispatch ensemble.
    namespace export {[a-z]*}
    namespace ensemble create -parameters str\
      -command [namespace current]::My
    # Object constructor:
    proc Create {str} {
        list [namespace which My] $str
    }
    # Public methods:
    proc serialize {str} {return $str}
    proc append {str str2} {
        Create "$str$str2"
    }
    proc appendString {str strObj} {
        Create "$str[{*}$strObj serialize]"
    }
    # Public alias for object constructor:
    interp alias {} [namespace current] {} [namespace current]::Create
}

you can do

set MyString [String "foo "]               
set AnotherString [String "bar."]
set aLine [{*}[{*}[{*}$MyString appendString $AnotherString] append "\n"] serialize]

which I think is effectively what was asked for (though I might be mistaken about printString being a serialize operation).


GPS: That looks bizarre with all the {*} syntax. I can do it this way:

 proc String s {
         set obj [objstructure]

         $obj ::add-keys string $s
         $obj ::add-method append [list String-append $obj]
         $obj ::add-method printString [list String-printString $obj]

         return $obj
 }

 proc String-append {obj append} {
         set s [$obj string]
         append s $append
         $obj string $s
         return $obj
 }

 proc String-printString {obj chan} {
         puts $chan [$obj string]
         return $obj
 }

 set aLineString [[[String "Hello World"] append "!  Give me a cookie!"] printString stdout]
 #prints to stdout: "Hello World!  Give me a cookie!"
 
 #if you want to cleanup do: rename $aLineString {} or add a destroy method that does that.
 #Reference counting could be done too, with release and retain messages.

tb - :) - My question was more about a more convenient syntax for cascading message posts. If you have a series of messages to be sent to the same object, then why always construct a full message post? Given, that every method that doesn't return a certain value, instead returns self, one could wright:

 SomeObject doSomething appendTo [AnotherObject copesWith anArgument] print

RS Umm.. usual OO models as I know them treat one method call as a unit. Your proposed calling style is certainly doable, but would involve some parsing of the arguments to decide which are method names, and which are arguments to methods. I'm not sure whether it's worth that when you can just code (as usual):

 $obj doSomething
 $obj appendTo [$obj2 copeswith $arg]
 $obj print

which looks still better readable to me... :^)


DKF: Here's an example (created to solve the Rosetta Code problem on active objects[2 ]) that shows an integrator class (using the trapezium method). Note the use of clock microseconds as part of the integration step and coroutine/yield to make the outer controller.

package require Tcl 8.6
oo::class create integrator {
    variable e sum delay tBase t0 k0 aid
    constructor {{interval 1}} {
        set delay $interval
        set tBase [clock microseconds]
        set t0 0
        set e { 0.0 }
        set k0 0.0
        set sum 0.0
        set aid [after $delay [namespace code {my Step}]]
    }
    destructor {
        after cancel $aid
    }
    method input expression {
        set e $expression
    }
    method output {} {
        return $sum
    }
    method Eval t {
        expr $e
    }
    method Step {} {
        set aid [after $delay [namespace code {my Step}]]
        set t [expr {([clock microseconds] - $tBase) / 1e6}]
        set k1 [my Eval $t]
        set sum [expr {$sum + ($k1 + $k0) * ($t - $t0) / 2.}]
        set t0 $t
        set k0 $k1
    }
}

set pi 3.14159265
proc pause {time} {yield [after [expr {int($time * 1000)}] [info coroutine]]}
proc task {script} {coroutine task_ apply [list {} "$script;set ::done ok"];vwait done}
task {
    integrator create i
    i input {sin(2*$::pi * 0.5 * $t)}
    pause 2
    i input { 0.0 }
    pause 0.5
    puts [format %.15f [i output]]
}

It should be possible to write a cleverer task system so that users just write a bunch of (apparently) linear tasks and then start the task manager. Not quite sure what the best way to write that down is yet.