Thingy: a one-liner OO system

Clipped from the Tcl chatroom on Feb 5, 2001:

suchenwi: This weekend I was thinking about minimal OO and came to the following one-liner:

 proc thingy name {proc $name args "namespace eval $name \$args"} 

(MS: see at bottom for a faster version using interp alias)

suchenwi: This is very poor, no inheritance at all. It just sugars the namespace wrapping, which in turn allows instance "variables" and "methods".

 thingy foo        
 foo set bar 1                ;#== set foo::bar 1
 foo proc grill x {subst $x!} ;#== proc foo::grill x {subst $x!}
 foo grill sausages           ;#== foo::grill sausages

miguel: Not bad at all! It does have the nice property that properties and methods *are* regular tcl vars and procs.

suchenwi: And, as I just tried, it can be nested:

 foo thingy baz 
 foo baz set id 42            ;#== set foo::baz::id 42
 foo baz proc wow x {subst x?} 

rmax: seems to be related to this one: Namespace resolution on unknown command

suchenwi: rmax: Exactly. At that time, Larry Smith proposed that feature for the Tcl core, but like often, you can do it yourself - in a one-liner, in this case...

rmax: suchenwi: I prefer to write "proc foo baz wow {x} {subst x?}" It is more readable IMHO.

suchenwi: Of course you can do it like this. But for the parser, {x} is "x" is x. YM foo baz proc wow {x} {subst $x?}

rmax: Yes, I know that x == {x} . My point was, that the semantic of "proc foo baz wow {} {...}" is more obvious than "foo baz proc wow {} {...}"

suchenwi: Oh. Yes, I just reread that Wiki page. The advantage of the above plaything is minimal effort, and orthogonality e.g. with "set", where "set foo bar" could not be taken as retrieving the value of foo::bar...

suchenwi: Methods could be dynamically shared like in UFO; variables via upvar.

miguel: On closer looks, it is (almost) perfect; good thinking, RS! Inheritance will pose some challenges though:

  • (a) inheritable procs will need to have the object pointer passed to them ("self/this/@")
  • (a') inheritable procs should refer to everything by global name
  • (b) inheriting a proc means copying it (as with waytos ...); what with compiled commands?

miguel: As it is, you can send not only single commands but also longer scripts (commands separated by ';' and properly quoted). If you restrict yourself to single commands, the following variante should be faster:

miguel: Well (back to thingys): it is faster, but less elegant:

 proc thingy name {
     namespace eval $name
     proc $name args "namespace inscope $name \$args"
 } 

miguel: It only works for single commands, but uses the faster eval on pure lists (I think, haven't timed it ...)


rmax: This adds copying of prototype objects:

 proc new {name} {

    uplevel 1 thingy $name

    set current [uplevel 1 namespace current]
    set parent [uplevel 1 namespace parent]
    foreach variable [info vars ${current}::*] {
        set vars $variable
        if {[array exists $variable]} {
            set vars [list]
            foreach n [array names $variable] {
                lappend vars ${variable}($n)
            }
        }
        foreach var $vars {
            namespace eval ${parent}::$name \
                [list set [lindex [split $var ::] end] [set $var]]
        }
    }
    foreach cmd [info procs ${current}::*] {

        set arglist [list]
        foreach arg [info args $cmd] {
            if {[info default $cmd $arg def]} {

                set arg [list $arg $def]
            }
            lappend arglist $arg
        }
        namespace eval ${parent}::${name} \
            [list proc [lindex [split $cmd ::] end] \
                $arglist [info body $cmd]]
    }
 }

rmax: I think, I just found a bug in your oneliner from this morning: it places all thingies (even the nested ones) in the global namespace.

rmax: here is one, that nests correctly:

 proc thingy name {
   proc [uplevel 1 namespace current]::$name args \
      "namespace eval $name \$args"
 }

suchenwi: Ah, I see. YM if the caller is in some namespace himself.

rmax: OK, the namespaces are nested as expected but the procs are all in the global namespace.

rmax: ... so you can't have "foo proc bar" and "baz proc bar" at the same time.

suchenwi: I can! The original one-liner that started this discussion creates ::foo::grill and ::bar::grill which are independent.

rmax:

 proc thingy name {proc $name args "namespace eval $name \$args"} 
 thingy foo 
 foo thingy foo1 
 puts [info commands foo*] => foo foo1 

suchenwi: Yes, I see.

rmax: namespace children :: foo* => foo foo1


BOOP is another super-simple all-Tcl OOP framework, in the same spirit as Thingy.


MS An almost-equivalent of the original one-liner is

   proc thingy name {interp alias {} $name {} namespace eval $name}

To correct the nesting behaviour as per rmax's comments, do instead

   proc thingy name {
       set name [uplevel 1 namespace current]::$name
       interp alias {} $name {} namespace eval $name
   }

The difference is that calling $name with no arguments will create the namespace in the original version, but cause an error in this one. This one is also faster in current tcl:

   [mig@mini mig]$ tclsh                  
   % info patch
   8.4.1
   % proc thingy name {proc $name args "namespace eval $name \$args"}
   % proc thingy2 name {interp alias {} $name {} namespace eval $name}
   % thingy a 
   % thingy2 b
   b
   % time {a set x 1} 10000
   36 microseconds per iteration
   % time {b set x 1} 10000
   23 microseconds per iteration

PAK I had trouble getting the interp alias one liner to work within my small object system because namespace eval flattens the list.

   % proc thingy2 name {interp alias {} $name {} namespace eval $name}
   % thingy2 a
   a
   % a puts "hello world"
   can not find channel named "hello"

Instead I needed 'namespace inscope', which turned it into a two liner because inscope does not create the namespace:

   % proc thingy3 name {
      namespace eval $name {}
      interp alias {} $name {} namespace inscope $name
   }
   % thingy3 b
   % b puts "hello world"
   hello world

PAK Adding a bit of sugar for creating object ids and deleting objects, thingy does make for a very compact object system:

   namespace eval obj {
    variable id 0
     proc thingy {} {
        variable id
        set obj [namespace current]::obj[incr id]
        namespace eval $obj {}
        interp alias {} $obj {} namespace inscope $obj
        $obj proc delete { args } {
            rename [namespace current] {}
            namespace delete [namespace current]
        }
        return $obj
    }
   } 

Since every object carries around its own methods, instead of having a class construct you can use a simple proc to populate your object:

   proc Bat {{ball foo}} {
    set obj [obj::thingy]
    $obj set v $ball
    $obj proc hit.ball ball { variable v; set v $ball }
    $obj proc ball {} { variable v; return $v }
    return $obj
   }

   % set b [Bat]
   ::obj::obj1
   % puts [$b ball]
   foo
   % $b hit.ball bar
   bar
   % puts [$b ball]
   bar
   % $b delete

This is not the most efficient in terms of creation time or memory, but dispatch is fast and the implementation is simple. You even get inheritance if instead of calling obj::thingy your class proc calls the constructor for some other class. Chaining destructors is a little bit messy, but you can do it if you rename delete to delete#myclass and call delete#myclass at the end of myclass's delete proc. The same trick will work for any other method that needs to call to its parent method. Instance variables can be traced in widgets using e.g., -textvariable [set b]::v.

It would be nice to clean up the object when there are no more references to it, but short of an array style implementation based on trace unset, or maybe resorting to a C implementation, this doesn't seem possible.


Thingy OO with classes adds class functionality to thingy in a style similar to XOTcl.


OO libraries another one-liner like approach but very different


DDG - 2021-08-26 - Thingy in Action! The Readme of pandoc-tcl-filter at http://htmlpreview.github.io/?https://github.com/mittelmark/DGTcl/blob/master/pandoc-tcl-filter/Readme.html contains an example on how to create svg files using Thingy.

DDG - 2021-08-28 - I created a minimal package out of the Thingy SVG creator called tsvg where a separate Download available via gitdown: tsvg package . Documentation will follow ...

DDG - 2021-08-30 - Documentation for the tsvg package - the Thingy SVG writer - is now here: http://htmlpreview.github.io/?https://github.com/mittelmark/DGTcl/blob/master/pandoc-tcl-filter/lib/tsvg/tsvg.html

DDG - 2021-09-06 - Documentation for the tdot package - the Thingy DOT writer - is now here: http://htmlpreview.github.io/?https://github.com/mittelmark/DGTcl/blob/master/lib/tdot/tdot.html