ensemble extend

Difference between version 52 and 53 - Previous - Next
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.


<<categories>> namespace ensemble | object orientation