Version 3 of Extending TclOO with metaclasses

Updated 2014-09-20 15:23:40 by aspect

Overview

metaclass is a concept introduced by TclOO, which is most explicitly visible in the info object command:

  info object isa metaclass ''object''
    This returns whether object is a class that can manufacture classes (i.e. is oo::class or a subclass of it).

So what are they good for? Well, the first thing aspect expected them to help with is extending TclOO, in the sense of adding commands which can be used on object definition scripts.

The reason for extending with metaclasses instead of adding commands to oo::define should become clear on reading TclOO Tricks: what might appear fixed concepts, like classvariable or classmethod, are actually quite loose: in different object systems they can behave quite differently! TclOO is flexible enough to accommodate multiple definitions, but we can't all expect to share a namespace without conflicts. "snit in TclOO" and "XOTcl in TclOO" needs to coexist peacefully. This page is an attempt to show one way that could work.

The code below has been only proven by running it by hand, and Inspecting TclOO to ensure the created objects and classes look correct. It could do with some cleaning up, and will probably need it before anyone other than aspect can run it, but the point is to read it, critique the approach and maybe learn something about TclOO along the way.

Known issues/todos:

  • metaclass's constructor not calling next precludes metaclasses being combined with mixin. Fix that.
  • reliance on aspect's local prelude of procs like map and debug needs to be removed
  • tests need to become proper tcltests with proper coverage
  • complete coverage of TclOO Tricks in this safer style
  • improve the narrative. I don't think much documentation is in order, as the code is quite short and changes little, but narrative examples will be very valuable
  • namespace and package

Discussion

Code

# a metaclass is a place for defining additional "class definition" methods, in addition
# to those normally available via oo::define.
#
oo::class create metaclass {
    superclass oo::class
    constructor {args} {
        if {[info object class [self]] eq [self class]} {
            ;# if we're defining a metaclass:
            oo::define [self] superclass [self class]
        } else {
            ;# we're defining an instance:
            oo::define [self] superclass oo::class
        }

        set myns [self namespace]::ns   ;# namespace for evaluating creation script
                                        ;# to which all our methods are aliased:
        foreach cmd [info object methods [self] -all] {
            if {$cmd ni {new create destroy}} { ;# object and class methods don't count
                interp alias {} ${myns}::$cmd {} [self] $cmd
            }
        }
        tailcall namespace eval $myns {*}$args  ;# in lieu of [next]
    }
}

;# wrap all the oo::define commands as methods on the class:
foreach cmd [map {namespace tail} [info commands ::oo::define::*]] {
    oo::define metaclass method $cmd args "
        oo::define \[self\] $cmd {*}\$args
    "
}


## random dependencies:

proc -- args {}         ;# noop
proc debug {subcmd s} { ;# should be sufficient to make it run
    puts "\[DEBUG\]: [uplevel 1 [list subst $s]]"
}
proc map {cmdPrefix args} {
    set argNames [iota [llength $args]]
    foreach name $argNames argList $args {
        lappend cmd $name $argList
    }
    set cmdArgs [lmap a $argNames {subst {\$$a}}]
    lmap {*}$cmd {
        uplevel 1 $cmdPrefix [subst $cmdArgs]
    }
}
proc iota {a {b {}}} {
    if {$b eq ""} {
        set b $a
        set a 0
    }
    for {set r {}} {$a<$b} {incr a} {
        lappend r $a
    }
    return $r
}

# ClassyVars:
#   classvariable defines a variable that lives on the class, and is [namespace upvar]ed
#   as an object variable into each instance's namespace on creation.
#
# It is the equivalent of:
#   oo::class create Cnted {
#     variable n
#     constructor {} {
#        my eval namespace upvar [info object namespace [self class]] n n
#        debug log {[incr n]}
#     }
#     method who {} {
#        debug log {[incr n]}
#     }
#   }

metaclass create ClassyVars {
    variable Classvars
    constructor args {
        debug log {[self] - [self class]}
        set Classvars {}
        next {*}$args
        oo::define [self] mixin [ClassyVarMixin new $Classvars]
    }
    method classvariable {name} {
        lappend Classvars $name
        uplevel 1 [list variable $name]
    }
}

oo::class create ClassyVarMixin {
    superclass oo::class
    constructor {classvars} {
        set vars [lmap v $classvars {list $v $v}]
        set vars [concat {*}$vars]
        oo::define [self] constructor {args} [format {
            my eval namespace upvar [info object namespace [self class]] %1$s
            next {*}$args
        } $vars]
    }
}

ClassyVars create Counted {
    classvariable n
    constructor args {
        debug log {making [incr n]}
    }
    method who {} {
        debug log {[incr n]}
    }
}


# AbstractBase is a metaclass which provides "abstractmethod"
# an "abstractmethod" must be defined in (direct) subclasses
# this requirement is enforced at the end of the class constructor
# via a mixin:
oo::class create AbstractMixin {
    superclass oo::class
    constructor {abstractmethods} {
        oo::define [self] constructor {args} [format {
            debug log {Constructing an abstract instance [self] that needs %1$s}
            next {*}$args
            set mymethods [info class methods [self] -all]
            foreach m %1$s {
                if {$m ni $mymethods} {
                    throw {CLASS ABSTRACTMETHOD} "abstract method $m not provided in [self]!"
                }
            }
        } [list $abstractmethods]]
    }
}

metaclass create AbstractBase {
    variable abstractmethods
    constructor args {
        set abstractmethods {}
        next {*}$args
        oo::define [self] mixin [AbstractMixin new $abstractmethods]
    }
    method abstractmethod {name} {
        lappend abstractmethods $name
    }
}
                                                  


# now attempt to use it
AbstractBase create Channish {
    abstractmethod read
    abstractmethod write
    # method, constructor, etc
}

Channish create Try {
    method read {args} {}
    method write {} {}
}

-- Channish create Try2 {
    method read {args} {}
} ;# error!

                                                       
# a classmethod (Ruby-like version) is a method that can be called on a class, and on subclasses of that class, but not on their instances
#
# The simplest way to put a method on a class is like this:
#  oo::class create Foo {
#    self method bar {} {}
#  }
#
# Or:
#  oo::class create Foo {}
#  oo::objdefine Foo bar {} {}
#
# Both of which permit:
#  Foo bar
#
# But to get on the superclass inheritance chain, it needs to be a class method on Foo's class.
#

metaclass create ClassyMethods {
    variable Classmethods
    constructor args {
        uplevel 1 [format {
            oo::class create %1$s.Class {
                superclass oo::class
            }
        } [list [self]]]
        next {*}$args
        oo::objdefine [self] class [self].Class
    }
    method classmethod {name args body} {
        debug show {[self]}
        tailcall oo::define [self].Class method $name $args $body
    }
    method superclass {class} {
        #debug do oo::objdefine [self] mixin $class
        tailcall oo::define [self].Class superclass ${class}.Class
    }
}

;# we want to start with this definition:
ClassyMethods create ActiveRecord {
    classmethod find args {
        debug show {[self] finding}
    }
}

ClassyMethods create Table {
    superclass ActiveRecord
}

Table find

# the above definition expands to something like the following:
#  ClassyMethods create ActiveRecord.Class {
#      method find args {
#          debug show {[self] finding}
#      }
#  }
#  ClassyMethods create ActiveRecord {
#      class ActiveRecord.Class
#  }
#  
#  ClassyMethods create Table.Class {
#      superclass ActiveRecord
#  }
#  ClassyMethods create Table {
#      superclass Table.Class
#  }
#