This is the second in a series of posts about the Tcl namespace command. Each subsequent post will cover more complex uses of namespaces. While this topic is strictly related to Tcl only, it could help you when developing more complex Glyph scripts.
The topics to be covered in this post depend upon a clear understanding of concepts outlined in Tcl Namespaces (Part 1). If you haven’t already, then please review them before continuing.
Subcommands
If you look at the Tcl command manual pages, you will find that many commands support subcommands. One example would be the Tcl string command with its 19 subcommands. A Glyph example would be the pw::Application command and its more than 70 subcommands!
In the example code below equal, map, and range are subcommands of the string command. Similarly, getVersion and getCAESolver are subcommands of the pw::Application command.
A Tcl command with one or more subcommands is also known as a command ensemble. The Tcl namespace command can be used to create your own command ensembles.
package require PWI_Glyph 2.18.0 set s1 "Hello World!" set s2 "hello world!" set fromToPairs {Hello Goodbye World Birdie} puts "string" puts " equal => [string equal $s1 $s2]" puts " equal -nocase => [string equal -nocase $s1 $s2]" puts " map => '[string map $fromToPairs $s1]'" puts " range => '[string range $s2 6 end-1]'" puts "pw::Application" puts " getVersion => '[pw::Application getVersion]'" puts " getCAESolver => '[pw::Application getCAESolver]'" # Output: # string # equal => 0 # equal -nocase => 1 # map => 'Goodbye Birdie!' # range => 'world' # pw::Application # getVersion => 'Pointwise V18.0R2C5' # getCAESolver => 'CGNS'
The RangeInt Namespace
Before we get into ensembles, let’s take a look at a namespace called RangeInt. RangeInt supports the creation of named integer values with an associated min and max range. Attempting to assign a value outside this range will result in a Tcl error being triggered. Some example usage of RangeInt is given at the end of the code listing. RangeInt defines a variable named params_ and four procs named add, set, get, and check.
The params_ variable is a Tcl dictionary that is used to store the range and value for each of the named integers.
The add proc is used to create a new named value.
The set, and get procs are used to manipulate the integer values.
The check proc is used to validate values.
namespace eval RangeInt { variable params_ [dict create] proc add { name {val 0} {min inf} {max inf} } { variable params_ dict set params_ $name MIN $min dict set params_ $name MAX $max RangeInt::set $name $val } proc set { name val } { variable params_ if {[check $name $val >= MIN] && [check $name $val <= MAX]} { dict set params_ $name VAL $val return } ::set min [dict get $params_ $name MIN] ::set max [dict get $params_ $name MAX] return -code error "Invalid value: $min <= $val <= $max" } proc get { name {defaultVal 0} } { variable params_ return [expr {[dict exists $params_ $name VAL] ? \ [dict get $params_ $name VAL] : $defaultVal}] } proc check { name val op key } { variable params_ ::set limit [dict get $params_ $name $key] return [expr [list "$limit" == "inf" || $val $op $limit]] } } RangeInt::add i1 888 ;# range -infinity ... +infinity RangeInt::add i2 5 0 20 ;# range 0 ... 20 RangeInt::add i3 -3 -10 ;# range -10 ... +infinity RangeInt::add i4 -5 inf -5 ;# range -infinity ... -5 puts "i1 == [RangeInt::get i1]" puts "i2 == [RangeInt::get i2]" puts "i3 == [RangeInt::get i3]" puts "i4 == [RangeInt::get i4]" puts {} RangeInt::set i1 500 puts "i1 == [RangeInt::get i1]" puts {} catch {RangeInt::set i2 21} msg puts "error '$msg'" puts "i2 == [RangeInt::get i2]" # Output: # i1 == 888 # i2 == 5 # i3 == -3 # i4 == -5 # # i1 == 500 # # error 'Invalid value: 0 <= 21 <= 20' # i2 == 5
Namespace Command Ensembles
Converting a namespace into a command ensemble is simple. First, you add a call to the namespace export command for each proc that you want to be an ensemble subcommand. Then, to actually create the ensemble, add a call to the namespace ensemble create command. This call must be made after all the subcommand procs have been exported. The bolded code fragments below show these changes.
Please notice that the check proc is not exported. It is a (private) helper proc that is only to be used by the namespace itself. Since check was not exported, attempting to call RangeInt check will result in an “unknown or ambiguous subcommand” error.
namespace eval RangeInt { variable params_ [dict create] namespace export add proc add { name {val 0} {min inf} {max inf} } { variable params_ dict set params_ $name MIN $min dict set params_ $name MAX $max RangeInt::set $name $val } namespace export set proc set { name val } { variable params_ if {[check $name $val >= MIN] && [check $name $val <= MAX]} { dict set params_ $name VAL $val return } ::set min [dict get $params_ $name MIN] ::set max [dict get $params_ $name MAX] return -code error "Invalid value: $min <= $val <= $max" } namespace export get proc get { name {defaultVal 0} } { variable params_ return [expr {[dict exists $params_ $name VAL] ? \ [dict get $params_ $name VAL] : $defaultVal}] } # do not export check - it is for internal use only proc check { name val op key } { variable params_ ::set limit [dict get $params_ $name $key] return [expr [list "$limit" == "inf" || $val $op $limit]] } namespace ensemble create } # No more RangeInt:: needed! RangeInt add i1 888 ;# range -infinity ... +infinity RangeInt add i2 5 0 20 ;# range 0 ... 20 RangeInt add i3 -3 -10 ;# range -10 ... +infinity RangeInt add i4 -5 inf -5 ;# range -infinity ... -5 puts "i1 == [RangeInt get i1]" puts "i2 == [RangeInt get i2]" puts "i3 == [RangeInt get i3]" puts "i4 == [RangeInt get i4]" puts {} RangeInt set i1 500 puts "i1 == [RangeInt get i1]" puts {} catch {RangeInt set i2 21} msg puts "error '$msg'" puts "i2 == [RangeInt get i2]" puts {} puts "check == [RangeInt::check i2 5 <= MAX]" ;# direct call ok catch {RangeInt check i2 5 <= MAX} msg puts "error '$msg'" # Output (same as above): # i1 == 888 # i2 == 5 # i3 == -3 # i4 == -5 # # i1 == 500 # # error 'Invalid value: 0 <= 21 <= 20' # i2 == 5 # # check == 1 # error 'unknown subcommand "check": must be add, get, or set'
Namespace or Ensemble?
There are no hard and fast rules when choosing between plain old namespace procs (RangeInt::add) or going the extra step of exporting procs as ensemble subcommands (RangeInt add). They both achieve the important encapsulation and scoping desired when building a complex Tcl or Glyph library.
I find that the copious use of :: in a Tcl script can be distracting and make the code harder to read. I lean towards using command ensembles for everything except the outer-most namespace. For instance, Glyph uses the pw namespace to protect the other nested, Pointwise specific command ensembles. That is, the pw namespace is not itself an ensemble. However, the nested namespaces such as pw::Grid are implemented as ensembles with subcommands like pw::Grid getAll.
The code below is a simplified implementation of the nested pw::Grid and pw::Database command ensembles inside the plain old pw namespace.
namespace eval pw { # The pw::Grid command ensemble namespace eval Grid { namespace export getAll proc getAll { args } { ... } namespace export getCount proc getCount { args } { ... } namespace ensemble create } # The pw::Database command ensemble namespace eval Database { namespace export getAll proc getAll { args } { ... } namespace export getCount proc getCount { args } { ... } namespace ensemble create } } # pw::Grid getAll, pw::Grid getCount, pw::Database getAll, and # pw::Database getCount now exist
Until Next Time
This post covered the creation of namespace command ensembles. I will be covering additional namespace wizardry in future posts. If you want to learn more about namespaces, then take a look at the full Tcl namespace documentation.
This Is How I Glyph – Tcl Namespaces (Part 3) | Another Fine Mesh
Pingback: This Is How I Glyph – Tcl Namespaces (Part 1) | Another Fine Mesh
Pingback: This Is How I Glyph – Tcl Namespaces (Part 3) | Another Fine Mesh