ensemble extend

Extending an ensemble of routines is one of the fundamental techniques for organizing routines in programs.

See Also

stacking, by Larry Smith
Performs a similar task.
dict getnull, by AMG
When the specified item doesn't exist, returns an empty string rather than raising an error.
ensemble objects
A simple TclOO emulation of namespace ensembles.
oclib.tcl , by samoc
Provided extend_proc , which is similar .

Description

In Tcl an ensemble of routines is often exposed as a single routine whose first argument is the name of the routine in the ensemble to execute. This is the basis of object-oriented programming in Tcl. To extend an existing ensemble:

  1. Populate a namespace with the routines that are to be added to the ensemble.
  2. Rename the original ensemble routine.
  3. Create in its place a new ensemble routine that exposes the extension routines, and falls back to the original ensemble for the original routines.

If the routines in the original ensemble are implemented in a namespace, it would be a bad idea to drop new routines into that namespace because it would require knowledge of all the routines already in that namespace. For example, the namespace might implement its own set that does something entirely different from set. Any routine dropped into the namespace would have to be aware of this and make sure to choose the right set. This approach does not compose well. In general then, the right approach is to extend a routine by wrapping it.

Here is an implementation of extend that keeps the extension routines neatly separated from the original set of ensemble routines:

namespace eval ensemble {
    # extend an ensemble-like routine with the routines in some namespace
    proc extend {routine extension} {
        if {![string match ::* $routine]} {
            set resolved [uplevel 1 [list ::namespace which $routine]]
            if {$resolved eq {}} {
                error [list {no such routine} $routine]
            }
            set routine $resolved
        }
        set routinens [namespace qualifiers $routine]
        if {$routinens eq {::}} {
            set routinens {}
        }
        set routinetail [namespace tail $routine]

        if {![string match ::* $extension]} {
            set extension [uplevel 1 [
                list [namespace which namespace] current]]::$extension
        }

        if {![namespace exists $extension]} {
            error [list {no such namespace} $extension]
        }

        set extension [namespace eval $extension [
            list [namespace which namespace] current]]

        namespace eval $extension [
            list [namespace which namespace] export *]

        while 1 {
            set renamed ${routinens}::${routinetail}_[info cmdcount]
            if {[namespace which $renamed] eq {}} break
        }

        rename $routine $renamed

        namespace eval $extension [
            list namespace ensemble create -command $routine -unknown [
                list apply {{renamed ensemble routine args} {
                    list $renamed $routine 
                }} $renamed
            ]
        ]

        return $routine
    }
}

In the following example, file is extended with newer and newerthan:

namespace eval fileextension {
    proc newer {a b} {
    puts [list eh [namespace which file]]
       #expr {[file mtime $a] > [file mtime $b]}
    }

    proc newerthan {mtime path} {
       expr {[file exists $path] && ([file mtime $path] > $mtime)}
    }
}
ensemble extend file fileextension

In the next example, modify is added to dict:

# extra useful dict commands

namespace eval dictextension {
    proc modify {var args} {
       upvar 1 $var dvar
       foreach {name val} $args {
          dict set dvar $name $val
       }
    }
}
ensemble extend dict dictextension 

Exending a namespace ensemble

PYK 2016-10-14 2020-01-26: extend, presented below, adds a routine to a namespace ensemble. Therefore, it only works with namespace ensemble routines, and not generally with other ways of implementing ensembles of routines. Furthermore, instead of accepting the name of an existing routine, it acts like proc, accepting a procedure specification and creating that procedure in the namespace for the ensemble routine. It also assumes that the namespace of the ensemble has the same name as the ensemble routine itself. Because of these caveats, it isn't as useful as the general technique of wrapping an ensemble routine, as presented above.

#! /usr/bin/env tclsh

package provide extend 1.0
package require tcl 8.5
 
# extend a command with new subcommands
proc extend {cmd subcmd subspec body} {
    namespace eval [uplevel 1 [list namespace which $cmd]] [string map [
        list %subcmd [list $subcmd] %subspec [list $subspec] %body [list $body]] {
        if {[namespace which [namespace tail [namespace current]]] ne "[
            string trimright [namespace current] :]::[
            namespace tail [namespace current]]"} {

            ::rename [::namespace current] [::namespace current]::[
                ::namespace tail [::namespace current]]
            ::namespace export *
            ::namespace ensemble create -unknown [list ::apply [list {ns subc args} {
                ::return [::list ${ns}::[::namespace tail $ns] $subc]
            } [namespace current]]]
        }
        puts [list creating %subcmd in [namespace current]]
        ::proc %subcmd %subspec %body
    }]
}

Example use:

extend file newer {a b} {
    return [expr {[file mtime $a] > [file mtime $b]}]
}

extend file newerthan {mtime path} {
  return [expr {[file exists $path] && ([file mtime $path] > $mtime)}]
}

Dynamically populating the map of a namespace ensemble

In a comp.lang.tcl posting dated Fri, 04 Apr 2014 09:25:30 DKF posted an example of using the ensemble's -unknown parameter to lazily apply extensions. A version of extend using this technique:

proc extend {ens script} {
    namespace eval $ens [concat {
        proc _unknown {ens cmd args} {
            if {$cmd in [namespace eval ::${ens} {::info commands}]} {
                set map [namespace ensemble configure $ens -map]
                dict set map $cmd ::${ens}::$cmd
                namespace ensemble configure $ens -map $map
            }
            return "" ;# back to namespace ensemble dispatch
                      ;# which will error appropriately if the cmd doesn't exist
        }
    }   \; $script]
    namespace ensemble configure $ens -unknown ${ens}::_unknown
}

Note that new extensions defined in this way will not appear in the ensemble's map until they are used, so the default error message is misleading.


PYK 2016-10-14: Here is DKF's version with some changes to avoid namespace collisions, and using apply instead of proc:

proc extend {ens script} {
    uplevel 1 [string map [list %ens [list $ens]] {
        namespace ensemble configure %ens -unknown [list ::apply [list {ens cmd args} {
            ::if {$cmd in [::namespace eval ::${ens} {::info commands}]} {
                ::set map [::namespace ensemble configure $ens -map]
                ::dict set map $cmd ::${ens}::$cmd
                ::namespace ensemble configure $ens -map $map
            }
            ::return {} ;# back to namespace ensemble dispatch
                        ;# which will error appropriately if the cmd doesn't exist
        } [namespace current]]]
    }]\;[list namespace eval $ens $script]
}

Dict Extensions by Napier

Napier 2015-12-27 --

I really like ES6 Javascript's capabilities to work with objects such as const { key1, key2 } = myObject, so I decided to give myself similar functionality with a dict pull command. One thing I am not sure of, is if setting an empty string is the proper thing to do when a value doesn't exist. I would like to handle it similar to javascript, but Tcl doesn't have a "null" option which could be used to default to false

I know this is somewhat similar to dict update or dict with, but the syntax is a bit simpler and it's designed for its exact purpose, except that it only unpacks the requested keys and will create the variables so they may be used without info exists in cases that is too verbose.

The resulting operation with extend:

set tempDict [dict create foo fooVal bar barVal]
dict pull $tempDict foo bar rawr
puts $foo        ; # % fooVal
puts $bar        ; # % barVal
puts $rawr       ; # % ""

Read More / See Code Dict Extensions

DKF: A dict update with an empty body will have the same effect, but requires that you specify the variable name separately to the key, and a dict with with an empty body will also have a related effect, but that expands all the keys to variables.

Page Authors

CMcC
Posted a version of extend in 2006 that evaluated a script directly in the namespace of existing ensemble. This was problematic because the new routines could be affected in unexpected ways depending on was in the namespace to begin with. The more general approach is to consider the existing namespace a closed implementation and leave alone.
PYK
Provided the modern version of extend that avoids mucking about in the original namespace.