This is the third 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 both Tcl Namespaces (Part 1) and Tcl Namespaces (Part 2). If you haven’t already, then please review them before continuing.
Objects are Good
If you have done any Pointwise Glyph scripting, then you know that Glyph is an object oriented (oo) Tcl package that supports datatypes such as pw::Point. These datatypes follow the create/manipulate/delete paradigm.
Many instances (a.k.a. objects) of a datatype can be created. Each object has its own internal variables that can be manipulated independently from all other objects using the object’s subcommands. The code below demonstrates the oo create/manipulate/delete paradigm using the pw::Point datatype.
To create a point object, we call the pw::Point create command. The value returned is captured in the variable dbPt. As can be seen in Manipulate section of the code, $dbPt supports subcommands just like a namespace command ensemble. That is because $dbPt is a namespace command ensemble!
When the $dbPt object is no longer needed, a call to the $dbPt delete command removes the $dbPt object namespace and all of its internal data from the system.
package require PWI_Glyph 2.18.0 # Create set dbPt [pw::Point create] puts "dbPt's object name == '$dbPt'" # Manipulate $dbPt setPoint {1.0 2.0 3.5} puts "dbPt getXYZ == [$dbPt getXYZ]" puts "dbPt isConstrained == [$dbPt isConstrained]" # Delete $dbPt delete # $dbPt no longer exists - this will fail catch {$dbPt isConstrained} msg puts "dbPt isConstrained == $msg" # Output # dbPt's object name == '::pw::Point_1' # dbPt getXYZ == 1.0 2.0 3.5 # dbPt isConstrained == 0 # dbPt isConstrained == invalid command name "::pw::Point_1"
RangeInt Revisited
Part 2 of this series introduced the RangeInt command ensemble. While RangeInt is functional, its usage tended to be verbose (RangeInt command name args). In addition, you can’t pass the named values into procs because the values are all hidden within RangeInt itself. As shown in the code below, only the value’s name can be passed to a proc and the proc would then need to call one of the RangeInt commands using that name.
# Using non-object oriented RangeInt from Part 2 source "RangeInt-ensemble.tcl" proc calcProgress { cnt total name } { RangeInt set $name [expr {int(100.0 * $cnt / $total)}] } RangeInt add percent 0 0 100 calcProgress 12 200 percent puts "percent: 12 / 200 == [RangeInt get percent]%" # Output # percent: 12 / 200 == 6%
If RangeInt was like pw::Point, the code would be more flexible. For instance, an object of any type could be passed to a proc as long the object supports the needed subcommands as demonstrated in the code below.
source "RangeInt-oo.tcl" proc calcProgressObj { cnt total obj } { $obj set [expr {int(100.0 * $cnt / $total)}] } set percent [RangeInt create 0 0 100] calcProgressObj 12 200 $percent puts "percent: 12 / 200 == [$percent get]%" $percent delete # This works if $other supports "set N" and "get" set other [OtherType create] calcProgressObj 20 400 $other puts " other: 20 / 400 == [$other get]%" $other delete # Output # percent: 12 / 200 == 6% # other: 20 / 400 == 5%
Let’s explore using namespaces to make RangeInt object oriented just like pw::Point.
RangeInt create
The first step on the road to an object oriented implementation of RangeInt is the need for a RangeInt create subcommand. This command will do the work needed to establish the new object’s command ensemble, initialize its variables, and then return the namespace name to the caller. The create proc is shown below.
namespace eval RangeInt { variable cnt_ 0 ;# used to create unique object names namespace export create proc create { {val 0} {min inf} {max inf} } { set ns [namespace current] # Build unique object name == "${ns}::_N" set objName "${ns}::_[incr ${ns}::cnt_]" # Create the object command ensemble namespace eval $objName [set ${ns}::proto_] # Init the object - error if $val is out of range ${objName}::ctor $val $min $max return $objName } namespace ensemble create ;# create the RangeInt ensemble #---------------------------------------------------------- # RangeInt object ensembles support these subcommands variable proto_ { # this is defined in the next section } ;# end variable proto_ }
The create proc first constructs a unique name (objName) by concatenating the namespace name stored in $ns with the incremented namespace variable cnt_. This produces names of the form ::RangeInt::_N.
Next, the object’s ensemble is created by the call:
- namespace eval $objName [set ${ns}::proto_]
After variable substitution, the command evaluated above would be similar to:
- namespace eval ::RangeInt::_1 $::RangeInt::proto_
The variable $::RangeInt::proto_ contains commands that define the new object’s namespace ensemble. We will take a closer look at this variable in the next section. This is similar to how we created the RangeInt ensemble in Part 2. Except here, the ensemble name is different for each object created.
Finally, the new object ensemble is initialized (constructed) with a direct call the the namespace proc ${objName}::ctor and the name of object’s namespace ensemble, $objName, is returned to the caller.
RangeInt::proto_
The RangeInt ensemble has been expanded below to include the value of the RangeInt::proto_ variable. Notice that it looks a lot like a namespace ensemble. Actually, if you were to replace variable proto_ below with namespace eval proto_ you would have a valid namespace ensemble named proto_. However, we don’t want one ensemble. We want many, identical ensembles that all support the same procs and variables.
This is how RangeInt create works. Every time RangeInt create is called, it uses the ensemble definition stored in RangeInt::proto_ to create a uniquely named, yet functionally identical ensemble. The ensemble name returned by RangeInt create is the object’s name and it provides access to the ensemble commands.
RangeInt::proto_ defines the three variables min_, max_, and val_. It also exports the three procs set, get, and delete. It also defines the unexported, helper procs ctor and check. As mentioned above, ctor is called by RangeInt create to initialize each new object. While check is used by the $object set subcommand to validate incoming values.
namespace eval RangeInt { variable cnt_ 0 ;# used to create unique object names namespace export create proc create { {val 0} {min inf} {max inf} } { set ns [namespace current] # Build unique object name == "${ns}::_N" set objName "${ns}::_[incr ${ns}::cnt_]" # Create the object command ensemble namespace eval $objName [set ${ns}::proto_] # Init the object - error if $val is out of range ${objName}::ctor $val $min $max return $objName } namespace ensemble create ;# create the RangeInt ensemble #---------------------------------------------------------- # RangeInt object ensembles support these subcommands # variable proto_ { variable min_ inf ;# object's range min variable max_ inf ;# object's range max variable val_ 0 ;# object's current value # constructor - NOT EXPORTED proc ctor { val min max } { variable min_ $min variable max_ $max [namespace current]::set $val ;# error if out of range } # helper - NOT EXPORTED proc check { val op lim } { return [expr [list "$lim" == "inf" || $val $op $lim]] } namespace export set proc set { val } { variable min_ variable max_ if { [check $val >= $min_] && [check $val <= $max_] } { variable val_ $val return $val_ } return -code error "Value $val not in ($min_, $max_)" } namespace export get proc get {} { variable val_ return $val_ } namespace export delete proc delete {} { namespace delete [namespace current] } namespace ensemble create ;# create the object ensemble } ;# end variable proto_ }
Using RangeInt
Some usage examples are given below to demonstrate the new oo functionality available in the enhanced RangeInt.
# Usage set vA [RangeInt create 22 -5 25] ;# range -5 ... 25 puts "vA's object name is '$vA'" puts "vA == [$vA get]" puts "vA == [$vA set -3]" catch {$vA set 99} msg puts "vA == $msg" $vA delete catch {$vA set 0} msg puts "vA == $msg" puts {} set vB [RangeInt create 1 0 5] ;# range 0 ... 5 puts "vB's object name is '$vB'" puts "vB == [$vB get]" puts "vB == [$vB set 5]" catch {$vB set -1} msg puts "vB == $msg" $vB delete catch {$vB set 0} msg puts "vB == $msg" # Output # vA's object name is '::RangeInt::_3' # vA == 22 # vA == -3 # vA == Value 99 not in (-5, 25) # vA == invalid command name "::RangeInt::_3" # # vB's object name is '::RangeInt::_4' # vB == 1 # vB == 5 # vB == Value -1 not in (0, 5) # vB == invalid command name "::RangeInt::_4"
Until Next Time
This post covered the creation of object oriented 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.
Pingback: This Is How I Glyph – Tcl Namespaces (Part 2) | Another Fine Mesh