another minimal Tcl object system (XOTcl like syntax)

Summary

by P.R.

See Also

XOTcl binary extension written in C

Description

namespace eval prns {
    variable count 0
    namespace export obj 
}

# args:
# name - name of new object
# ?-initcmd procname   - additional constructor proc (fully-qualified name of any available proc)
# ?-ns nsname   - namespace where object should be created
# ?-var1 val ?-var2 val  - initial variables
# obj X    ;# create object X (namespace ::X,command ::X::X,interp alias ::X)
# obj Y -ns myns  ;# create object myns::Y (namespace ::myns::Y,command ::ns::X::Y, interp alias ::Y)


proc prns::obj {name args} {
   if {$name eq "new"} {
       set name _obj__[incr prns::count]
   }
   set newname [init $name {*}$args]
   #inconsistency  
   if {[lsearch [info commands] $name] == -1} {
       return [interp alias {} $name {} $newname\::$name]
   } else {
       return $newname\::$name
   }
}

proc prns::init {name args} {
   set ns {} 
   set initcmd {} 

   if {[set idx [lsearch $args "-ns"]] != -1} {
       set ns [string trim [lindex $args [incr idx]] ::]
   }

   set newname $ns\::$name

   namespace eval $newname {} 
       
   foreach {-var val} $args {
       if {${-var} eq "-ns"} {continue}
       if {${-var} eq "-initcmd"} {
           set initcmd $val
           continue
       }
       if {[string index ${-var} 0] eq "-"} {
           variable $newname\::[string trimleft ${-var} -] $val
       }
   }

   proc $newname\::[namespace tail $newname] {command args} {
       if {$command eq "set" || $command eq "unset"} {
          variable [lindex $args 0] 
       }
       $command {*}$args  
   }

   #proc $newname\::new args {
   #    eval prns::obj _obj__[incr prns::count] -ns [namespace current] $args 
   #}

   proc $newname\::obj {name args} {
       set newobj [prns::obj $name {*}$args]
       [self_] mixin $newobj
       if {[llength [info procs [$newobj namespace current]::init]]} {
           namespace inscope [$newobj namespace current] init
       }
       return $newobj
   }

   proc $newname\::configure {args} {
       foreach {-var val} $args {
           set [namespace current]\::[string trimleft ${-var} -] $val
       }
   }

   proc $newname\::cget {-var} {
       if {[info exists [namespace current]\::[string trimleft ${-var} -]]} {
           return [set [namespace current]\::[string trimleft ${-var} -]]
       } else {
           return -code error "Option ${-var} does not exist"
       }
   }

   proc $newname\::info_ {cmd args} {
       switch -- $cmd {
           parent  {return [namespace parent]::[namespace tail [namespace parent]]}
           childs { set l {}
               foreach chld [namespace children] {
                   lappend l [namespace tail $chld]
               }
               return $l
           }
           vars  {  set l {}
               foreach var [::info vars [namespace current]::*] {
                   lappend l [namespace tail $var]
               }
               return $l
           }
           default {::info $cmd {*}$args}
       }
   }

   proc $newname\::instvar args {
       foreach var $args {
           uplevel 1 variable $var 
       }
   } 

   proc $newname\::instproc args {
       variable expprocs
       proc {*}$args
       set expprocs([lindex $args 0]) 1 
       return
   }

   proc $newname\::my_ args {
       [self_] {*}$args 
   }

   proc $newname\::self_ {} {
       return [namespace current]::[namespace tail [namespace current]] 
   }

   proc $newname\::destroy {} {
       set dispcmd [lindex [info level -1] 0]
       catch {interp alias {} [namespace qualifiers $dispcmd] {}}
       catch {rename [namespace qualifiers $dispcmd] {}}
       namespace delete [namespace current]
       return
   }

   proc $newname\::mixin {obj} {
       variable privvars
       variable expprocs
       if {![string equal [info commands $obj] $obj]} {
           return -code error "Target object $obj not exist"
       }
       set currns [namespace current] 
       set targns [$obj namespace current]
       foreach cmd [info procs ${currns}::*] {
           set cmd [namespace tail $cmd]
           if {![info exists expprocs($cmd)]} {continue}
           set pargs ""
           foreach arg [info args $cmd] {
               if {[info default $cmd $arg defval]} {
                   append pargs "\{$arg \{$defval\}\} "
               } else {
                   append pargs "$arg "
               }
           }
           proc $targns\::$cmd $pargs [info body $cmd]
       }
       foreach var [info vars ${currns}::*] {
           set var [namespace tail $var]
           if {[info exists privvars($var)]} {continue}
           variable $var
           if {[array exists $var]} {
               upvar 0 $var arr
               variable $targns\::$var
               array set $targns\::$var [array get arr]
           } elseif {[exists $var]} {
               variable $targns\::$var [set $var]
           } 
       }
       return
   }

   proc $newname\::newchild {objnew args} {
       if {[info procs $objnew] eq $objnew} {
           return -code error "Child $objnew (proc) already exist"
       }
       set obj [prns::init $objnew -ns [namespace current] {*}$args]
       proc [namespace current]::$objnew args {
           set mycmd [lindex [info level 0] 0]
           $mycmd\::[namespace tail $mycmd] {*}$args           
       }
       return $obj
   }

   proc $newname\::exists {var} {
       variable $var
       if {[array exists $var]} {
           return 1
       }
       return [info exists $var]
   }

   proc $newname\::privvar {args} {
   variable privvars
       foreach var $args {
           set privvars($var) ""
       }
   }

   if {[llength [info commands [lindex $initcmd 0]]]} {
       proc $newname\::init__ args [info body [lindex $initcmd 0]]
       #execute additional init proc
       $newname\::init__ {*}[lrange $initcmd 1 end]

   }
   
   return $newname

}

Examples

# create new object/class o1 "namespace ::o1 , object proc ::o1::o1, interp alias ::o1"
prns::obj o1 -color red
o1 instproc foo {} {    ;# create method of object "o1" named "foo"
    instvar color 
    puts $color
}

# invoke method foo
o1 foo
# -> red

# constructor proc
o1 instproc init args {
    puts "init [my_ set color]"
}

# o2 inherits from o1
o1 obj o2
# -> init red

# new autonamed object
set myobj [prns::obj new]

# export methods and variables from o1 to $myobj
o1 mixin $myobj

# new child object o2 "created namespace ::o1::o2, proc ::o1::o2::o2"
o1 newchild o3

# a method of o1::o3 object "proc ::o1::o3::self"
o1 o3 instproc self {} {
    puts [self_]
} 

# invoke method self of object o3
o1 o3 self
# -> ::o1::o3::o3
o1 o3 mixin o1   ;# export child "o3" methods and vars to parent object "o1"
o1 self
# -> ::o1::o1
o1 destroy       ;# destroy object o1 and all its children

DDG This looks really nice. However I missed the methods configure and cget (although I think they are not xotcl like ...). So I was adding them: Now this is possible:

prns::obj oc;# -> oc
oc configure -test testval -test2 testval2
oc cget -test ;# -> testval
prns::obj oc2 -test newval ;# -> oc2
oc2 cget -test ;# -> newval

Personally I like this more than:

oc2 set test ;# -> newval
oc2 set test foo

Because you can configure more than one option in one command.

It should be possible to set the options only at object creation time and return an error if trying to set an unknown option later on.

DDG 2004-02-17: Added check if option exists for cget. A Question of Design: Should options be created only at object creation?

prns::obj oc3 -sample 1 -sample2 2 ;# oc3
oc3 cget -sample ;# 1
oc3 cget -sample2 ;# 2
oc3 cget -sample3 ;# Option -sample3 does not exist

DKF: On the last point, of course not! Dynamic reconfigurability is very Tcl.

More generally though, the problem with this style of option processing is that it only works well when all options take exactly one parameter and all values are set by option. Otherwise, you run into major problems with variadic option processing (e.g., working out whether a value begins with a - because it is an option name or because the option value looks like that). TclOO rejected the approach because of the problems; it's the single truly major change from XOTcl to TclOO, with everything else following as a consequence. (OK, the implementations are very different inside too, but that's a much less significant point.)