Version 155 of TIP #257: Object Orientation for Tcl

Updated 2007-06-06 12:48:53 by jcw

Sept 26, 2005 See http://tip.tcl.tk/257

SUMMARY

TIP #257 is a proposal to put a small-but-powerful object-oriented programming system into the Tcl core (for 8.5) where everyone will be able to rely on it. Its feature set is to be based mostly on XOTcl, but with the addition of features for improved support for being the core of Snit. It should also be usable on its own.


IMPLEMENTATION NOTES

DKF 19 Aug 2006: There is now a partially working version of the tip257 implementation for people to test out. It is at about the point where you can create an object, give it some methods, and run the methods. However, constructors, destructors and inheritance do not yet work.

To get the source code, you need to check out the branch of CVS that I'm using for the development. You want to get from the usual tcl repository, but you want the branch tip-257-implementation-branch. Thus, assuming you've just got anonCVS access, you do this (the anon password is empty in case you're wondering, and the second line is split):

 cvs -d:pserver:[email protected]:/cvsroot/tcl login
 cvs -d:pserver:[email protected]:/cvsroot/tcl -z9 co \
        -r tip-257-implementation-branch -d tcl-and-tip257 tcl

Building on both Unix and Win should be supported (no idea about OSX). There is a (small) test file in the Tcl test suite, oo.test. If you want to know to basically use the code, look there first as the code passes the test now. No proper docs yet; use the TIP itself there for the moment.

Example session:

 % oo::object create foo
 ::foo
 % oo::define foo {method bar {} {puts "Hello, World!"}}
 % foo
 wrong # args: should be "foo method ?arg ...?"
 % foo ?
 unknown method "?": must be bar or destroy
 % foo bar
 Hello, World!
 % oo::define foo {method spong {} {my bar; next}}
 % foo spong
 Hello, World!
 no superclass method implementation
 % set errorInfo
 no superclass method implementation
     while executing
 "next"
     (method "spong" line 1)
     invoked from within
 "foo spong"

DKF 20 Aug 2006: Constructors now work (but there's only one meaningful class to use them on). On the down side, now know that the global command doesn't work in methods and constructors properly... (now fixed)

DKF 21 Aug 2006: Destructors, method forwarding (which used to crash), export and unexport now all added. There seems to be some kind of intermittent crash in the destructor code, probably due to memory not being managed quite right, but it doesn't crash on my home machine so tracking it down is slightly awkward. Export and unexport work, but won't work once we get inheritance in, especially in the MI case. Need to rethink how to implement them. :-(

OK, the destructor crash has been pinned down as being due to destructors firing when the OO system as a whole is going away. That degree of cleanup isn't something I've attempted to do yet.

DKF 23 Aug 2006: Single inheritance now working (or a good approximation of it; it's passing several tests).

RLH Is it the intention then of only allowing single inheritance and not multiple to avoid the "diamond" effect of inheritance? Or is this just a step in that direction?

DKF 24-25 Aug 2006: Multiple inheritance (diamond effects resolved using XOTcl rules) now working. Still got filters and mixins to do, but they're simple extensions of MI. (Does this answer your question, RLH? :-)) RLH Indeed it does. : )

DKF 26 Aug 2006: Most of object and class teardown now implemented. Memory leaks should be largely (but not yet completely) banished.

DKF 27 Aug 2006: Teardown now done, allowing more of test suite to pass. Filters now supported on objects (not class-wide).

DKF 28 Aug 2006: Per-object mixins and changing-an-object's-class now done. Mainly introspection facilities left in the C part of the development of this functionality. RLH You *are* sleeping I hope. : )

DKF 28 Aug 2006 (no, 29 Aug; it's late): Partial implementation of object cloning. Method cloning not working right, and class cloning completely wrong. (And I do this instead of watching television, which is why I have so much time.) RLH I haven't had television for 12 years now. : )

DKF 29 Aug 2006: Fixed crashes that were stopping the test suite from passing on Linux. They were caused by trying to run destructors in an interpreter which was cleaning up (a bad thing!), what happens when an object is deleted the "wrong way round" and the complex tangle of what happens when the object system core goes away. Thanks to dgp for helping me debug this.

DKF: 30 Aug 2006: Added the start of the introspection facilities called for in the TIP.

DKF: 31 Aug 2006: Non-class object cloning done.

DKF: 02 Sep 2006: Class cloning done, and oo::definer metaclass implemented. All current tests pass, so what's left are believed to be sins of omission.

DKF: 17 Sep 2006: Nearly finished the oo::struct class.

DKF: 18 Sep 2006: I believe oo::struct is now finished (there are some really ugly cases in there!) but still need to write more tests. Also namespace exported all public oo commands.

DKF: 19/20 Sep 2006: oo::struct now done, along with a tested (and much more efficient) variable method.

DKF: 25 Sep 2006: Added support for class filters. Removed parameters as they're trivial to add using a script. Finished defining something for self; the semantics are not exactly as in the original TIP because I found that the meaning of some parts was not so clear. Only significant missing feature is "class mixins", which I don't understand and don't currently propose to implement.

DKF: 1 Oct 2006: Discovered that class filters don't work how they are documented to work; need to figure out how I think they should work before deciding whether to change the code or the spec. On the plus side, there's a C API now (though the functions are currently declared in tclOO.h since I don't wish to tinker with the stubs table in a branch) and everything apart from the class filter stuff is now consistent with the TIP.

DKF: 4 Oct 2006: Completed introspection; the code is now fully introspectable (which it wasn't before, ho hum.)

DKF: 8 Oct 2006: Following experiments into what the XOTcl Method Traversal Order really is, I've added class mixins. Implementation probably isn't perfect, but this is now a functionally-complete OO core.

DKF: 21 Oct 2006: Code quality now much better. The C API is expanding in size to something much closer to production-ready, and it is now also much easier to plug in OO systems on top. In particular, there is now a (C API only) metadata mechanism for both classes and objects, and the introspection code is now a full ensemble, allowing extra bits and pieces to be plugged in there. Also, the code to copy an object is now its own command, oo::copy, since it didn't fit well with oo::define but I don't want it to be a method in itself (copying an object or class may require careful consideration of what this means for the object in question.)


DISCUSSION

Moved to TIP #257 Discussion.


TESTING

MJ - I am playing around with the new functionality to get a feel for it. To do this I am porting some code from XOTcl to tip OO. When using constructors I don't seem to be able to define multiple instance variables. Am I doing something wrong in the code below?

 oo::class create Test {
  constructor {args} {
   variable a b
   set a [lindex $args 0]        
   set b [lindex $args 1]
  }
 }

 set a [Test new a b]

 info object $a vars 

 # returns a, I would have expected {a b} to be returned

TR - 'variable a b' would set b as the value of variable a. I don't think this is what you intended. Did you want this?

 variable a [lindex $args 0] b [lindex $args 1]

Just my $0.02

MJ - You are of course correct. Rereading the variable section shows I should have used [my variable] instead. However I cannot seem to get this to work at all. Some class and instance variable examples would be very welcome.

 ::oo::class create Test {
  method a {} {
   my variable a
   set a test
   puts $a
  }
 }
 [::Test new] a

 variable name "a" illegal: must not contain namespace separator

DKF: Looks like a bug. Not yet fixed. :-(

DKF: D'uh! Had the sense of a test inverted; now fixed.


TCLOO EXTENSION

DKF 18 May 2007: At last, after a long hiatus, I've had enough time to finish getting the code smashed into shape as a loadable TEA extension. It works for me (as in: successfully runs the test suite) on both Windows and Linux (built with gcc on both) as long as you're using a recent-enough version of Tcl (i.e. more recent than 8.5a6; there was an internal API that had to be modified to prevent crashes when info frame was used inside a method). Right now, there's no distribution, but you can check it out of cvs using:

 cvs -d:pserver:[email protected]:/cvsroot/tcl co oo

To build, just change into the base directory of the code and do a configure/make combo.

(Also includes experimental support for building a starkit version, as long as you have sdx on the path. Let me know if it works or not, as it doesn't work locally for other ugly reasons.)

04jun07 jcw - Some notes on trying to build this extension:

  • I'm used to doing just "make install", but that only works if "make install" also causes a normal "make"
  • am getting the following build error (MacOSX x86):
      install: /.../TclOO-0.1/library/*.tcl: No such file or directory
  • have worked around it by creating an empty library/blah.tcl for now :)

Ok, that woiked, great!

Now a usage question, if I may: is there some nice idiom for the following?

      my variable {*}[info vars [namespace current]::*]

IOW, set up methods to automatically have access to all object state as plain variables (as in Itcl). The above line would be added in front of all method definitions, using a small wrapper (am generating methods dynamically anyway).

This assumes that all variables are defined in the constructor.

escargo Does this use of variable contrast confusingly with the Tcl variable where something like that would assign the names of half the variables as the values of the other half of the variables?

jcw - Well, I'm just trying it out, so don't shoot me ;) - although so far I tend prefer this over the "variable" cmd.

escargo - I was just asking a question. It's just that having my variable work not like variable might be considered confusing.

jcw - Here's an example with some output which may help those who don't have TclOO yet:

  oo::class create dog {
          method a {} {
            puts a1-[namespace current]
            puts a2-[namespace path]
            foreach x [namespace path] {
              puts a3-$x-[info commands ${x}::*]
          }
            puts a4-[info vars [namespace current]::*]
            my variable e
            set e f
            puts a5-[info vars [namespace current]::*]
          }
          self.method b {} {
            puts b1-[namespace current]
            puts b2-[namespace path]
            foreach x [namespace path] {
              puts b3-$x-[info commands ${x}::*]
          }
            puts b4-[info vars [namespace current]::*]
            my variable e
            set e f
            puts b5-[info vars [namespace current]::*]
          }
  }

  dog create fifi
  fifi a
  dog b

  # Output:
  #   a1-::oo::Obj4
  #   a2-::oo::Helpers
  #   a3-::oo::Helpers-::oo::Helpers::self ::oo::Helpers::next
  #   a4-
  #   a5-::oo::Obj4::e
  #   b1-::oo::Obj3
  #   b2-::oo::Helpers ::oo
  #   b3-::oo::Helpers-::oo::Helpers::self ::oo::Helpers::next
  #   b3-::oo-::oo::InfoClass ::oo::class ::oo::InfoObject ::oo::object \
  #                                                     ::oo::copy ::oo::define
  #   b4-
  #   b5-::oo::Obj3::e

As you can see, each class and each instance gets its own namespace. Note also that class Dog ended up as namespace Obj3, even though it was constructed with a specific name (which is not necessarily globally unique, though).

DKF: I've been experimenting for a while now with OO systems, and a my variable that brings every variable in the object into scope is probably not what you want as it causes great problems in subclasses (i.e. clashes between subclasses' variables and local variables). Instead, I think that being able to pick up all the variables defined within a particular class would be far more useful. OTOH, it's not at all trivial to implement (I think I expose enough API that it can be done through the use of class metadata, but I'm not certain) so I'm not in a hurry to change things.

It was a deliberate decision to make my variable different from variable; the latter has syntax that is almost never what anyone wants, and so is a priori suboptimal. Hence I went for something that was more likely to be what you need. :-)

(escargo - Well, I wondered if you did it on purpose, and you did. And I see it was for what you think is a good reason.)

Also, as noted, the internal namespaces have names that are not necessarily those of the created object, and their names are deliberately not documented (i.e. they could change between point releases with no warning). Use the introspection facilities to go from one to the other.

APW 2007-06-05 @ jcw: I am working on an extended version of dkf's code suitable for Itcl, which would have solutions for some of the problems you have mentioned above. For example I am using "apply" and "namespace upvar" and "namespace unknown" to generate Itcl methods, which have access to all the class variables in the class hierarchy (if they are not private) etc. Using only the name of a variable as in Itcl is possible, same for call of Itcl methods without the "my" in front. If you are interested please contact me directly, the implementation is not yet ready for real testing.

@ dkf: If I know what I really need additionally for Itcl I will contact you and we can try to perhaps get a merged version. At the moment all my modifications are additional functionality so that the original functionality should work as before with also running all your tests without problems.


DKF: The following example is ripped from the documentation of next:

              oo::class create cache {
                  filter Memoize
                  method Memoize args {
                      # Do not filter the core method implementations
                      if {[lindex [self target] 0] eq "::oo::object"} {
                          return [next {*}$args]
                      }

                      # Check if the value is already in the cache
                      my variable ValueCache
                      set key [self target],$args
                      if {[info exist ValueCache($key)]} {
                          return $ValueCache($key)
                      }

                      # Compute value, insert into cache, and return it
                      return [set ValueCache($key) [next {*}$args]]
                  }
                  method flushCache {} {
                      my variable ValueCache
                      unset ValueCache
                      # Skip the cacheing
                      return -level 2 ""
                  }
              }

              oo::object create demo
              oo::define demo {
                  mixin cache
                  method compute {a b c} {
                      after 3000 ;# Simulate deep thought
                      return [expr {$a + $b * $c}]
                  }
                  method compute2 {a b c} {
                      after 3000 ;# Simulate deep thought
                      return [expr {$a * $b + $c}]
                  }
              }

              puts [demo compute  1 2 3]      → prints "7" after delay
              puts [demo compute2 4 5 6]      → prints "26" after delay
              puts [demo compute  1 2 3]      → prints "7" instantly
              puts [demo compute2 4 5 6]      → prints "26" instantly
              puts [demo compute  4 5 6]      → prints "34" after delay
              puts [demo compute  4 5 6]      → prints "34" instantly
              puts [demo compute  1 2 3]      → prints "7" instantly
              demo flushCache
              puts [demo compute  1 2 3]      → prints "7" after delay

Way cool, great separation of functionality. Next step: a Metakit backed cache? ;) -jcw


APW 2007-06-06 For discussion of Itcl related topics for a new implementation based on this TIP see tclOO missing features for Itcl


[ Category Object Orientation | Category TIP ]