Updated 2016-10-20 09:38:47 by dkf

Tcl's ensembles are handy, but sometimes you want to add a command to an existing one. This page presents some ways to do that.

See Also  edit

stacking, by Larry Smith
Performs a similar task.
dict get
Includes a dict getnull by AMG, which returns an empty string rather than raise an error when there is no such entry.
oclib.tcl, by samoc
Has a similar extend_proc command.

A Word of Warning  edit

Some examples on this page add new new commands into namespace of the ensemble, which can result in shadow core commands in the global namespace, which may cause other commands in the ensemble, to misbehave. For example, adding a set subcommand is almost guaranteed to cause problems!

PYK's refinements to CMcC's and DKF's versions below take more care to avoid this risk. There is also ycl shelf subcmd, which uses a namespace ensemble map to accomplish the task, allowing a subcommand like set to be mapped to a command named set_, which can be located either in the namespace of the ensemble or in any other namespace.
proc ::some_ensemble::set_ args {
    error [list {just kidding} $args]
}

shelf subcmd ::some_ensemble set set_

Or, if the command is in another namespace:
shelf subcmd ::some_ensemble:: set ::some_other_namespace::some_command

To use shelf subcmd with an ensemble that doesn't have a map, first create a map for it, perhaps using ycl ns ensemble commands:
foreach command [ensemble commands some_ensemble] {
    shelf subcmd some_ensemble $command
}

After that, use [shelf] subcmd to add more commands.

Another alternative is, Ensemble objects, which with the help of TclOO try to design around the problem for new ensembles.

CMcC's version (2006)  edit

Here's a simple bit of code to extend any ensemble-like command by means of tcl8.5's namespace ensemble command. CMcC 6Mar2006:
package provide extend 1.0
package require Tcl 8.5
 
# extend a command with a new subcommand
proc extend {cmd body} {
    if {![namespace exists ${cmd}]} {
        set wrapper [string map [list %C $cmd %B $body] {
            namespace eval %C {}
            rename %C %C::%C
            namespace eval %C {
                proc _unknown {junk subc args} {
                    return [list %C::%C $subc]
                }
                namespace ensemble create -unknown %C::_unknown
            }
        }]
    }

    append wrapper [string map [list %C $cmd %B $body] {
        namespace eval %C {
            %B
            namespace export -clear *
        }
    }]
    uplevel 1 $wrapper
}

Here is file, extended with two subcommands: newer and newerthan:
extend file {
    proc newer {a b} {
       return [expr {[file mtime $a] > [file mtime $b]}]
    }

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

Here is dict, extended with the modify subcommand:
# extra useful dict commands
extend dict {
    proc modify {var args} {
       upvar 1 $var dvar
       foreach {name val} $args {
          dict set dvar $name $val
       }
    }
}

PYK 2016-10-14: CMcC's implementation is subject to some quoting and robustness issues, I've created a variant of it that accepts as arguments a procedure specification instead of a complete script. The main advantage to this interface change is that the user doesn't have to worry about encountering an alternate proc in some namespace.
#! /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)}]
}

DKF's Version  edit

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  edit

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.