Search code examples
namespacestcl

Namespaces and procedures and scope inside namespaces


I'm trying to make a "safe" method of generating request ids for web sockets (just a desktop app not a real server) and want each socket to have its own id generator. All I'm doing is generating ids and recycling them after the request completes, such that the id doesn't grow unlimited throughout a user's session. I used an example concerning closures for a counter in JavaScript from David Flanagan's book and all seems to work well in Tcl but I'd greatly appreciate any advice on how to do this correctly and how I can test that these variables cannot be altered by the main program apart from calling one of the procedures within the namespaces. For example, is it possible to modify the gap list under the WEBS::$sock from the global namespace with [meant without] calling one of the procedures? Thank you.

Also, is there any difference between declaring namespace eval WEBS {} outside proc. ReqIdGenerator and using namespace eval WEBS::$sock inside the procedure? I can see that the results are the same for my little tests but wondered if there was any differences otherwise.

As an aside, in JS using the push and pop methods of arrays, it seems easier to recycle ids on a last-in-first-out basis; but using Tcl lists, it seems easier to use a first-in-first-out basis because using lassign with one variable assigns index 0 to the variable and returns the remaining elements as a new list. The equivalent of array.pop() seems to require more steps. Is that a correct observation? Thank you.

WARNING: There is an error in this code in that the namespace references $sock and it works only because it is a global variable. If it were not global, the code would throw and error. The best I could find thus far is in this question.

proc ReqIdGenerator {sock} {
  namespace eval WEBS {
    namespace eval $sock {
      variable max 0
      variable gap {}
      variable open {}
      variable sock $sock

      proc getId {} {
        variable max
        variable gap 
        variable open 
        if { [llength $gap] > 0 } {
          set gap [lassign $gap id]
          lappend open $id
          return $id
        } else {
          lappend open [set id [incr max]]
          return $id
        }
        chan puts stdout "Error in getId"
        return -1
      }
      proc delId {id} {
        variable max
        variable gap
        variable open
        if { [set i [lsearch $open $id]] == -1 } {
          return 1
        } elseif { [llength $open] == 1 } {
          reset
        } else {
          lappend gap [lindex $open $i]
          set open [lreplace $open $i $i]
        }
        return 0
      }
      proc reset {} {
        variable max 0
        variable gap {}
        variable open {}
      }
      proc getState {{prop "all"}} {
        variable max
        variable gap
        variable open
        variable sock
        if { $prop eq "all" } {
          return [list $max $gap $open]
        } elseif { $prop eq "text" } {
          return "State of socket $sock: max: $max; gap: $gap; open: $open"
        } else {
          return [set $prop]
        }
      }
    }
  }
}

set sock 123
ReqIdGenerator $sock
set sock 456
ReqIdGenerator $sock

# Add ids 1 through 10 to both sockets
for {set i 0} {$i<10} {incr i} {
  WEBS::123::getId
  WEBS::${sock}::getId
}
# Delete even ids from socket 456
for {set i 2 } {$i<11} {incr i 2} {
 WEBS::${sock}::delId $i
}
# Delete odd ids from socket 123
for {set i 1 } {$i<10} {incr i 2} {
 WEBS::123::delId $i
}
chan puts stdout [WEBS::123::getState text]
# => State of socket 123: max: 10; gap: 1 3 5 7 9; open: 2 4 6 8 10
chan puts stdout [WEBS::456::getState text]
# => State of socket 456: max: 10; gap: 2 4 6 8 10; open: 1 3 5 7 9

Solution

  • Lots of questions to unpack here.

    how I can test that these variables cannot be altered by the main program apart from calling one of the procedures within the namespaces

    You can't. There are no access controls within an interpreter. You can have multiple interpreters and there are strong access controls between them, but that's pretty heavyweight. However, it's conventional to not go rummaging around in a namespace that you don't own to peek at things you've not formally been told about on the grounds that they're liable to be changed at any moment without any sort of notification to you (usually not at runtime, but no guarantees!).

    A phrase I've seen used in the community is "If you break it, you get to keep all the pieces".

    For example, is it possible to modify the gap list under the WEBS::$sock from the global namespace with calling one of the procedures?

    I'm sure it is. Finding it might be tricky, but once you have the name you can change it.

    is there any difference between declaring namespace eval WEBS {} outside proc. ReqIdGenerator and using namespace eval WEBS::$sock inside the procedure?

    There, assuming you handle the possible differences in name resolution scope of the name of the namespace itself. (That doesn't matter for fully qualified names — names beginning with :: — but relative names might resolve differently.)

    The equivalent of array.pop() seems to require more steps. Is that a correct observation?

    Yes. 8.7 adds lpop to address this weakness.


    Your code appears to be reinventing objects. Use TclOO (or one of the other major object systems such as [incr Tcl] or XOTcl) for that; it's better at the job.

    oo::class create ReqIdGenerator {
        variable max gap open sock
        constructor {sock} {
            set max 0
            set gap {}
            set open {}
            set [my varname sock] $sock; # messy because formal parameter
        }
    
        method getId {} {
            if { [llength $gap] > 0 } {
                set gap [lassign $gap id]
                lappend open $id
                return $id
            } else {
                lappend open [set id [incr max]]
                return $id
            }
            chan puts stdout "Error in getId"
            return -1
        }
    
        method delId {id} {
            if { [set i [lsearch $open $id]] == -1 } {
                return 1
            } elseif { [llength $open] == 1 } {
                my reset
            } else {
                lappend gap [lindex $open $i]
                set open [lreplace $open $i $i]
            }
            return 0
        }
    
        method reset {} {
            set max 0
            set gap {}
            set open {}
        }
    
        method getState {{prop "all"}} {
            if { $prop eq "all" } {
                return [list $max $gap $open]
            } elseif { $prop eq "text" } {
                return "State of socket $sock: max: $max; gap: $gap; open: $open"
            } else {
                return [set [my varname $prop]]
            }
        }
    }
    
    set sock 123
    set s1 [ReqIdGenerator new $sock]
    set sock 456
    set s2 [ReqIdGenerator new $sock]
    
    # Add ids 1 through 10 to both sockets
    for {set i 0} {$i<10} {incr i} {
        $s1 getId
        $s2 getId
    }
    
    # Etc.