Posted to tcl by aspect at Thu Aug 27 13:00:15 GMT 2015view pretty

# why is this so subtle?
#
# because object methods, class methods, constructors, initialisation scripts, classes, metaclasses, 
# instances and subclasses are all concepts just a little bit too close to one another.
#
# But I think I've (finally!) nailed it.
#
#
# To best understand this, it helps to reflect on a few things:
#
# TclOO consists of four commands: oo::class, oo::object, oo::define, oo::objdefine.  The first two
# are objects:  they're tasty and the whole point of the exercise.  The other two are a bit funny.
# They're kinda like namespace ensembles with a -parameter, but if you check, that expansion doesn't
# hold:
#
#   % oo::define myclass method bark {} {puts woof}
#   % oo::define::method myclass bark {} {puts woof}
#   error
#
# They seem to work by inspecting the stack to see which object is being defined -- at least, that's
# the best method I've seen used in script.  Basically, they're a little bit perverse and resistant
# to extension in a way most things in Tcl aren't, which is why I kept picking at this like a scab.
#
# It's worth noting (accepting) that objects and classes are different.  Which is why there are these
# two definition commands.  And hard-to-articulate differences between class methods and object 
# methods, to say nothing of the weird involuted relationship between oo::class and oo::object.
#
# This module makes classes better, but does nothing for objects.  That will come later.
#
# An object is a command with a namespace tied to its lifetime.  And various properties relating to
# OO and Classes, but the namespace lifetime is really the nugget.
#
# A class is a special kind of object that can also (create) be the class of objects.  This is why
# there is no oo::objdefine::constructor, and why a mixin can only be a class.
#
# oo::class is special among classes in that its instances are classes.  This is why they get a create 
# method, but their instances don't.  We call it a metaclass.
#
# How do you make a metaclass?  By creating a class which is a subclass of oo::class:  this way its
# instances will have create methods and be able to act as classes.
#
# It is worth having [Inspecting TclOO] handy as you proceed.  Nothing beats looking under the covers.
#
catch {namespace delete meta}

namespace eval meta {

    ;#proc debug {args} { puts "D: [uplevel 1 subst $args]" }
    proc debug args {}

;# doesn't everybody use these?  [interp alias] + all those [namespace] calls is so verbose

    proc alias {alias cmd args} {
        if {![string match ::* $alias]} {
            set alias [uplevel 1 {namespace current}]::$alias
        }
        if {![string match ::* $cmd]} {
            set c [uplevel 1 [list namespace which $cmd]]
            if {$c eq ""} {
                return -code error "Could not resolve $cmd!"
            }
            set cmd $c
        }
        tailcall interp alias {} $alias {} $cmd {*}$args
    }

    proc unalias {args} {
        foreach cmd $args {
            interp alias {} [uplevel 1 [list namespace which $cmd]] {}
        }
    }

;# now we start:

    # The class of all classes ...
    oo::class create Metaclass {
        superclass oo::class
    }

    # .. has all the oo::define commands as methods.
    # they take an extra argument at the beginning: [$metaclass method $class name args body]
    #                                   .. compare: [oo::define $class method name args body]
    foreach cmd [info commands ::oo::define::*] {
        set tail [namespace tail $cmd]
        if {$tail eq "self"} continue

        oo::define Metaclass method $tail {cls args} [format {
            set cls [uplevel 1 [list namespace which $cls]]     ;# could avoid this with [tailcall]..
            debug log {defining %1$s on $cls}
            oo::define $cls %1$s {*}$args
        } $tail]
    }

    # The base class of all classes is an instance of Metaclass and a subclass of oo::class.
    # Got that?  No, neither have I.  But play along .. it works.
    #
    # As an instance of Metaclass, it gets all the methods defined above (eg [Class export $cls $name ...]).
    #
    # As a subclass of oo::class, its instances are classes with no special features.
    #
    Metaclass create Class0 {

        superclass oo::class

        # Class's constructor takes a script, which it evaluates
        # in the new class's namespace, temporarily augmented
        # with aliases to all of its own methods.
        constructor {{script ""}} {
            set class [info object class [self]]    ;# not [self class] !
            debug log {creating $class [self]}

            set cmds [info object methods $class -all]
            foreach cmd $cmds {
                alias $cmd $class $cmd [self]
            }

            try $script finally [list unalias {*}$cmds]
        }
    }
    ;# This is a drop-in extension for oo::class.  It has some extra methods, which should
    ;# not get in the way next to the normally-used [$cls create] [$cls new] and [$cls destroy].

    ;# Its constructor (class initialiser) behaves slightly differently, but these changes should
    ;# not be able to impact any but the most pathological constructor scripts.  See below.

    ;# Costs only occur at class creation time, which should be insignificant unless your name
    ;# is hypnotoad.  The aliases exist only as long as they need to, and in the class object's
    ;# namespace where they shouldn't be able to affect anything but the class's initialisation
    ;# script.

    ;# Normally only one command exists in this namespace (as with any object's namespace): [my]
    ;# Commands from oo::Helpers are also in the path (self, next, nextto), but nothing else.


;# !!! There is one loss !!!
    ;#  [oo::define::self], normally an alias to ~ [oo::objdefine [self class]], is gone
    ;# since we're running in the constructor.  I don't think that's a big loss, since it's
    ;# a confusing name coincidence and rarely used.  It needs exposure under a better name,
    ;# like [class].

    ;# I think we could restore [self] as a method on Metaclass, but it strikes me as risky.
    ;# Maybe that's not the case.  But I'm not sure I like the pun.


    ;# Some useful extensions are immediately visible:
    ;#
    ;#  * restore [self]-like behaviour through a different name ([class])
    ;#  * add [uplevel 1 namespace current] to the path during the initialisation script
    ;#    in fact, leave it there.  I want it more often than not.
    ;#
    oo::define Metaclass method class {cls cmd args} {   ;# restoration of oo::define::self
        debug log {class $cmd $cls [info object class [self]]!}
        oo::objdefine $cls $cmd {*}$args
    }
    Metaclass create Class {
        superclass ::meta::Class0
        constructor args {
            set p [namespace path]
            namespace path [list {*}$p [uplevel 1 {namespace current}]]
            next {*}$args
            ;#namespace path $p
        }
    }

}

;# Well .. but so what?

if 1 {
    namespace path [list meta {*}[namespace path]]

    ;# The net effect is close to zero:  Class has methods forwarding to oo::define's
    ;# pseudo-methods, and its constructor runs the new class's initialisation script
    ;# in the class's own namespace, with appropriate aliases in the namespace as though 
    ;# you were really there.
    ;#
    ;# Creating a class works pretty much the same:
    Class create C1 {
        method bark args woof
        variable foo

        ;# except this is neat:
        debug log {Creating [self] in [namespace current] (eq [info object namespace [self]])}
        ;# oo::class initialisers run on oo::define!  These run in the class's own namespace.
        ;#
        ;# This is a fairly big deal, since that's where most creative commands like to write
        ;# (like [proc]).
    }

    ;# we get this for free, which is just as well because it seems more distracting than useful:
    Class method C1 foo {args} {set foo {*}$args}

    ;# notice that these methods don't get in the way, as the only (object) methods we normally 
    ;# use from a class are [$cls create] [$cls new] and sometimes [$cls destroy].
    ;# 
    ;# This is a different thing (I think) than "class methods" as they exist in other languages.
    ;# But the concept appears pretty weak to me, so I don't think much is lost.  Note also that 
    ;# other Tcl object systems (which?) have taken an interpretation of class methods (or "class 
    ;# subcommands"?) similar to this.

    ;# but the magic comes here:
    Metaclass create Fancy {
        method accessor {cls args} {        ;# remember the extra cls argument!
            set cls [uplevel 1 [list namespace which $cls]]     ;# remember this bit too!
            foreach name $args {
                my variable $cls $name
                my method $cls $name args [format {
                    set %s {*}$args
                } [list $name]]
            }
        }
    }

    ;# now we could just:
    #oo::objdefine Class mixin Fancy
    ;# but in the interest of neighbourliness, let's leave our nice Class alone and
    ;# mess with a derivative:
    Metaclass create Klass {
        superclass ::meta::Class
    }
    oo::objdefine Klass mixin Fancy
    ;# notice that this used objdefine, not define, because it's affecting the class and not its instances.
    ;# a strong case could be made for making this a (object) method of Class.

    ;# of course, we can also do it with oo::objdefine ... or uplevel!
    oo::objdefine Klass method public {cls cmd name args} {
        my $cmd $cls $name {*}$args
        my export $cls $name
    }
    oo::objdefine Klass method private {cls cmd name args} {
        debug what
        uplevel 1 [list $cmd $name {*}$args]
        uplevel 1 [list unexport $name]
    }

    ;# now we can use that like this:
    Klass accessor C1 bar baz

    ;# or the better way:
    Klass create C2 {
        constructor args {
            lassign $args frop quaz
        }
        accessor frop quaz

        class method bar bar bar

        private method shush sh shh
        public method Brum {} gogogo
    }

    ;# notice that C2's instances bear no sign of being metaclass-derived.  This is good.

    ;# what you just saw is a SAFE way to extend object creation.   While I request that you squint
    ;# past my mixin directly on Class, notice that [accessor] as defined here need not conflict with
    ;# any other extension's use of the same command!  No more fighting over oo::define.  Or even
    ;# scribbling on it - that's just rude.
}

# Having done this, it's tempting to try a lot more.  Sensible namespace behaviour for oo::object
# initialisers;  using [info object namespace oo::class]::my CreateWithNamespace;  much more.  But
# it's a twisty maze of passages all alike, so go gently.