Dynamically adding methods to a Snit object

NEM 2008-10-18: A recent question from AM on comp.lang.tcl [1 ] asked whether it was possible to dynamically extend the methods that a Snit object supports after it has been created. Snit doesn't seem to have explicit support for this functionality built-in. However, it is still possible to achieve, and even quite elegantly, using the existing delegation facilities. Note that there may be other ways to do this, such as manipulating the namespace ensemble of a Snit object directly, but the approach here has the advantage of using only documented interfaces, and so should continue to work even if the internals of Snit change.

The approach taken is to use a separate environment object that stores a dynamic map of new methods, and then delegate to this environment from the snit object you want to extend. Here we use lambdas for the new methods, but you could also create procedures in the instance namespace.

package require Tcl  8.5
package require snit 2.2
snit::type methodenv {
    variable methods [dict create]
    method define {name params body} {
        set params [linsert $params 0 self]
        set f [list $params $body]
        dict set methods $name $f
        return $name
    }
    method apply {obj name args} {
        ::apply [dict get $methods $name] $obj {*}$args
    }
}

To enable this functionality for an object, simply install an environment as a component of your object:

snit::type foo {
    component env
    delegate method method to env as define
    delegate method * to env using "%c apply %s %m"
    constructor {} {
        install env using methodenv $self.env
    }
}
foo a
a method sub {a b} { expr {$a-$b} }
a method bar {} {
    puts "10-3 = [$self sub 10 3]"
}
a bar

There are some limitations here: only the $self variable is available in these "methods", so anything else will have to be retrieved/updated explicitly through that; also, the namespace in which the methods execute is incorrect (they execute in global ns).


The original example on clt was to define an extensible set of mathematical functions on an object as methods. The code for this example is also quite interesting, and demonstrates the same technique with some new twists, so is included here as well:

package require Tcl 8.5  ;# required
package require snit 2.2 ;# may work with earlier
# A function environment
snit::type funcenv {
    constructor {} {
        # Make things nice for expr
        namespace eval $selfns\::tcl::mathfunc \
            [list namespace path $selfns]
    }
    # Define a function:
    # env define f x y ... = expression
    method define {name args} {
        set body [list expr [lindex $args end]]
        set params [lrange $args 0 end-2]
        proc $selfns\::tcl::mathfunc::$name $params $body
        return $name
    }
    method apply {f args} {
        $selfns\::tcl::mathfunc::$f {*}$args
    }
}

snit::type numeric {
    component env
    delegate method define to env
    delegate method * to env using "%c apply %M"
    constructor {} {
        install env using funcenv $self.env
    }
    method interal {a b} { ... }
}
numeric create func
func define f x = {$x*exp(-$x)}
func f 1
func define g x = {$x-f($x)}
func define pi = acos(-1) 
# ... etc ...