This Is How I Glyph – Tcl Namespaces (Part 3)

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
    eval "namespace eval $objName \$${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_ {
    ...
  } ;# end 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:

  • eval “namespace eval $objName \$${ns}::proto_”

The eval command take the arguments passed to it and evaluates them with a recursive call to the Tcl interpreter. After variable substitution and character escaping, the script 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 later. 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
    eval "namespace eval $objName \$${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 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.

About David Garlisch

Illini by birth, Texan by choice.
This entry was posted in Applications, Software and tagged , , . Bookmark the permalink.

One Response to This Is How I Glyph – Tcl Namespaces (Part 3)

  1. Pingback: This Is How I Glyph – Tcl Namespaces (Part 2) | Another Fine Mesh

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s