Extending an ensemble of [routine%|%routines] is one of the fundamental techniques for organizing routines in programs. ** See Also ** [stacking], by [Larry Smith]: Performs a similar task. `[dict get%|%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. [https://github.com/samoconnor/oclib.tcl%|%oclib.tcl], by [samoc]: Provided `[https://github.com/samoconnor/oclib.tcl/blob/master/oclib/oc_ensemble-1.0.tm#L17%|%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 orientation%|%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. 1. Rename the original ensemble routine. 1. 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 [ECMAscript%|%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. <> namespace ensemble | object orientation