Skip to content

Instantly share code, notes, and snippets.

@brabect1
Last active June 15, 2024 04:11
Show Gist options
  • Save brabect1/d46f54e43e4b54392c04582f5e6077b3 to your computer and use it in GitHub Desktop.
Save brabect1/d46f54e43e4b54392c04582f5e6077b3 to your computer and use it in GitHub Desktop.
About Tcl ensembles #tcl #namespace #ensemble

Tcl Namespace Ensemble

Tcl ensemble is a dispatcher command. If you oversimplify, you can think of it as an alias. What a dispatcher command does is that it calls another command along with all arguments. Here is an example:

# everbody knows a `file` command, right
file dirname [pwd]

# how about this
::tcl::file::dirname [pwd]

The file command is an ensemble. Calling file dirname ... will eventually yield calling ::tcl::file::dirname .... If you fast forward, file <cmd> will (most likely) dispatch to ::tcl::file::<cmd>.

Note

The file dirname can be thought of as an alias to ::tcl::file::dirname. This is simplification, though, as the ensemble construct/mechanism is more powerful than just that.

Ensemble calling convention is <ensemble> <subcommand> <arg1> ... <argN>. Then in file dirname [pwd], the file is the ensemble, dirname is the subcommand and [pwd] becomes an argument passed by the ensemble to the subcommand.

From the user perspective, Tcl ensemble is a means for grouping related Tcl commands. This, indeed, is the purpose of namespaces, too; ensembles, however, can avoid lengthy command qualification and can do without using namespace import (and its risk of name clashing).

Basic Usage

Ensembles are coupled to namespaces as they are created by the namespace ensemble command (or an esemble, indeed, as it dispatches to ::tcl::namespace::ensemble). In the simplest case, namespace ensemble create creates "aliases" to all commands exported by the current namespace. The name of the ensemble aliases with the name of the current namespace. For example:

namespace eval foo {
    namespace export bar baz;

    proc bar {} {
        puts "[lindex [info level 0] 0]";
    }

    proc baz {} {
        puts "[namespace current]::baz";
    }

    proc bax {} {
        puts "[namespace current]::bax";
    }

    namespace ensemble create; #                       <--- simplest way of creating an ensemble
}

foo::bar
foo::baz
foo::bax

foo bar
foo baz
foo bax; # yields an ERROR as `bax` is not exported

Note that the implicit/default name of the new ensemble is the same as [namespace current]. Hence:

namespace eval ::foo::bar {
    namespace export *;

    proc bar {} {
        puts "[lindex [info level 0] 0]";
    }

    namespace ensemble create;
}

foo::bar::bar
foo::bar bar
bar bar; # yields an ERROR as `bar` is not an ensemble (nor a command)

Note

Note that the lately created foo::bar ensemble overrides the formely defined bar proc inside the foo namespace. Hence any of foo::bar, ::foo::bar and foo bar would yield wrong # args: should be "... subcommand ?arg ...?".

Custom Ensemble Name

You can control the name of the ensemble through the -command option to namespace ensemble create. The following example also shows, that you can create as many ensembles for a namespace as you will.

namespace eval ::foo::bar {
    namespace export *;

    proc bar {} {
        puts "[lindex [info level 0] 0]";
    }

    # creating a 1st ensemble (as an ensemble local to the current namespace)
    namespace ensemble create -command foobar;

    # creating a 2nd ensemble with a fully qualified names
    namespace ensemble create -command ::bar;
}

foo::bar::bar
foo::bar::foobar bar
bar bar

Note

Beware, though, that unless you fully qualify the new ensemble, it will create the ensemble in the current namespace. Hence the following would create the ::foo::bar::bar ensemble that makes it impossible to call the same named bar routine:

namespace eval ::foo::bar {
    namespace export *;
    proc bar {} { ... }
    namespace ensemble create -command bar;
}

::foo::bar::bar; # error
::foo::bar::bar bar; # error

Custom Subcommand Name

We can also control how ensemble subcommands are named and, indeed, have more fine grained control what all subcommands are supported by the eventual ensemble. This is through the -map <dict> option to namespace ensemble create. For example:

namespace eval bar {
    proc alpha {args} { puts "[lindex [info level 0] 0] ${args}"; }
    proc beta  {args} { puts "[lindex [info level 0] 0] ${args}"; }
    proc gamma {args} { puts "[lindex [info level 0] 0] ${args}"; }

    # !!! notice we export no procedure under this namespace but
    # still make any a part of the ensemble
    namespace ensemble create -map {
        alpha alpha
        beta beta
        gamma beta
        delta ::foo::bar
    };

}

bar alpha a b c
bar beta a b c
bar gamma a b c
bar delta; # notice this subcommand dispatches outside of `::bar` namespace
bar omega a b c; # ERROR as `omega` has not been mapped

The example shows quite a few things:

  • A subcommand name has nothing to do with the actual command being mapped, as in bar gamma.
  • The same command can be mapped several times, such as ::bar::beta is mapped to bar beta and bar gamma.
  • Mapping can target any command, even in different namespace, if proberly qualified. Such as in bar delta that dispatches to ::foo::bar.

The ensemble explicit mapping through namespace ensemble create -map ... can be queried through namespace ensemble configure -map. Notice that in the actual mapping the subcommands got fully qualified.

puts [namespace ensemble configure ::bar -map];
> alpha ::bar::alpha beta ::bar::beta gamma ::bar::beta delta ::foo::bar

Besides querying, namespace ensemble configure can also alter the ensemble. So we can add a new subcommand, for example:

# notice we have to fully qualify the `gamma` command if changing
# the ensemble outside its namespace context
namespace ensemble configure ::bar -map \
    [dict merge [namespace ensemble configure ::bar -map] {omega ::bar::gamma}]

bar omega a b c; # now ok

Note

The above example of adding a new subcommand shows, too, that the ensemble manipulation (incl. ensemble create) is not limited to the namespace it relates too.

There is a bit more we can do with the -map mapping. It takes the <key>-<value> pairs in the form a Tcl dictionary, where <key> is the subcommand name and the <value> is the command to which the ensemble dispatches. The <value> is indeed a list, such that the first, mandatory item is the command and the other, optional items are arguments to be passed along to the command when being dispatched by the ensemble. Here is an example:

namespace eval ::bar {
    namespace ensemble create -map {
        beta beta
        gamma {beta 1 2}
    };
}

bar beta a b c;
> ::bar::beta a b c

# notice that for the dispatching, the (fixed) arguments `1` and `2` from
# the map precede the (optional) arguments given to the ensemble call
bar gamma a b c;
> ::bar::beta 1 2 a b c

Note

Using ensemble for grouping related commands has an important but subtle advantage over a namespace. The ensemble can name subcommand arbitrarily without worries of name clashing. For example, no namespace can define the set command without clashing with the Tcl core set (and hence having severe limitations).

Ensemble can, though, easily map set to whatever it likes. So it is not uncommon to see something like this:

namespace eval whatever {
    proc Set {args} { ... }
    namespace ensebmle create -map { set Set ... }
}

whatever set ...

Unknown Subcommands

Misc

Original TIP 112

Ensembles manual (Tcl 8.5)

Examples of extending Tcl core functions (Tcler's wiki)

namespace eval td {
    proc _myproc {} {
        set n [dict get [info frame [info frame]] proc];
        puts "$n: [namespace current] vs. [namespace which $n]";
    }

    # Notice different visibility of the target procedure names
    # within the namespace.
    namespace ensemble create -map {
        myproc _myproc
        extern ::td::_extern
    }

    # this will export `_myproc`, but you could not use it as a part
    # of `td` ensemble (i.e. trying `td _myproc` would err); however,
    # creating the `::sdb` ensemble command will provide access to all
    # exported procedures
    namespace export _myproc;
    namespace ensemble create -command ::sdb;
}

namespace eval ydb {
    # import all of `td` namespace
    namespace import ::td::*;

    # re-export everything
    namespace export *;

    # create ensemble for all exported commands
    namespace ensemble create;
}

# A proc defined outside of the namespace
proc ::td::_extern {} {
    puts "[namespace current]";
}

td::_myproc
td myproc
#td _myproc; # error
td::_extern
td extern

puts "---"

sdb _myproc
#sdb myproc; # error
#sdb extern; # error

puts "---"

ydb::_myproc
ydb _myproc
#ydb::_extern; # error, `_extern` not part of `::td` and hence not imported
namespace eval foo {
namespace export bar baz;
proc bar {args} {
puts "[lindex [info level 0] 0]";
}
proc baz {} {
puts "[namespace current]::baz";
}
proc bax {} {
puts "[namespace current]::bax";
}
namespace ensemble create;
}
foo::bar
foo::bax
foo bar
foo baz
#foo bax
namespace eval ::foo::bar {
namespace export *;
proc bar {} {
puts "[lindex [info level 0] 0]";
}
namespace ensemble create -command foobar;
}
foo::bar::bar
foo::bar::foobar bar
#bar bar
#bar bar; # error
#::foo::bar; # error
#foo::bar; # error
#foo bar; # error
namespace eval bar {
proc alpha {args} { puts "[lindex [info level 0] 0] ${args}"; }
proc beta {args} { puts "[lindex [info level 0] 0] ${args}"; }
proc gamma {args} { puts "[lindex [info level 0] 0] ${args}"; }
namespace ensemble create -map {
alpha alpha
beta beta
gamma beta
delta ::foo::bar
};
}
puts [namespace ensemble configure ::bar -map]
bar alpha a b c
bar beta a b c
bar gamma a b c
bar delta
namespace ensemble configure ::bar -map \
[dict merge [namespace ensemble configure ::bar -map] {omega ::bar::gamma}]
bar omega a b c
namespace eval ::bar {
namespace ensemble create -map {
beta beta
gamma {beta 1 2}
};
}
bar beta a b c
bar gamma a b c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment