Version 2 of sproc

Updated 2009-01-26 16:23:25 by andy

AMG: I developed the following code so I could have multiple "instantiations" of a static-enabled proc, where each instantiation has its own independent copies of the static variables. All instantiations share the same bytecode-compiled proc body.


Implementation

proc sproc {name args statics script} {
   upvar #0 Sproc-$name data
   if {[info exists data]} {
      error "sproc \"$name\" already exists"
   }
   set data(next) 0
   set data(instances) [dict create]
   set data(lambda) [list $args\
      "dict with [list [namespace current]::Sproc-$name](\[list\
         \[namespace tail \[lindex \[info level 0\] 0\]\]\]) {$script}"]
   set data(statics) $statics
   proc $name {args} {
      set name [list [namespace tail [lindex [info level 0] 0]]]
      upvar #0 Sproc-$name data
      set others [info commands $name*]
      while {1} {
          set nameid $name$data(next)
          incr data(next)
          if {$nameid ni $others} {
              break
          }
      }
      dict set data(instances) $nameid ""
      set data($nameid) [dict replace $data(statics) {*}$args]
      proc $nameid {*}$data(lambda)
      trace add command $nameid delete [list apply {{name cmd args} {
         upvar #0 Sproc-$name data
         unset data([list [namespace tail $cmd]])
         dict unset data(instances) [namespace tail $cmd]
      }} $name]
      return $nameid
   }
   trace add command $name delete [list apply {{name args} {
      upvar #0 Sproc-$name data
      foreach instance [dict keys $data(instances)] {
         catch {rename $instance ""}
      }
      unset data
   }} $name]
   return $name
}

Documentation

Defining a sproc

[sproc name args statics script] creates a sproc called name that accepts arguments args, has static variables statics, and has body script. It returns name.

  • name can be any name.
  • args can be any proc-style argument list. Ordinary, defaulted, and variadic arguments are supported.
  • statics is a dict mapping from static variable names to initial values. Contrast this with defaulted arguments, in which each argument is a two-element list.
  • script can be any script. It does not need to do anything special in order to access or modify static variables or any other kind of variables.

Creating an instance

[name ?var1 val1 var2 val2 ...?], where name is a previously-defined sproc, creates an instance of the sproc with optionally overridden or augmented static variables var1, var2, etc. assigned to have initial values val1, val2, etc., respectively. It returns the name of the instance.

  • name must be a sproc already defined by [sproc], above.
  • var1, var2, etc. are the names of additional static variables to define for this instance. They can optionally be the same as names defined in the initial [sproc] invocation.
  • val1, val2, etc. are the initial values of the additional or overridden static variables.

Invoking an instance

[instance ?...?], where instance is a name returned by the name sproc invocation, calls the sproc instance. Arguments are passed as normal. The sproc script body is able to freely access and modify static variables, which persist between subsequent invocations of the same instance. Static variables are not shared between separate instances of the same sproc.

Deleting an instance

Deleting instance (a sproc instance) with rename instance "" removes it from the sproc's associated data structure.

Deleting a sproc

Deleting name (a sproc) with rename name "" deletes its associated data structure and all its instances.

Internal data structure

Each sproc has an associated data structure named Sproc-name, where name is the name of the sproc. It is an array containing the following variables:

  • Sproc-name(next): The numeric ID of the next instance to be created.
  • Sproc-name(instances): Dictionary whose keys are the names of all current instances of the sproc. The values are all empty string.
  • Sproc-name(lambda): Lambda definition of an instance of the sproc.
  • Sproc-name(statics): Default dictionary mapping from static variable names to initial values. This mapping can be augmented or overridden when creating an instance of the sproc.
  • Sproc-name(instance): Dictionary mapping from static variable names to current values for the sproc instance named instance.

Demonstration and testing

% sproc counter {{increment 1}} {value 0} {incr value $increment}
counter
% set p [counter]
counter0
% set q [counter value 10]
counter1
% $p
1
% $p 5
6
% $q 0
10
% # Show internal data structure
% parray Sproc-counter
Sproc-counter(counter0)  = value 6
Sproc-counter(counter1)  = value 10
Sproc-counter(instances) = counter0 {} counter1 {}
Sproc-counter(lambda)    = {{increment 1}} {dict with ::::Sproc-counter([list [namespace tail [lindex [info level 0] 0]]]) {incr value $increment}}
Sproc-counter(next)      = 2
Sproc-counter(statics)   = value 0
% # Demonstrate instance deletion
% rename $p ""
% parray Sproc-counter
Sproc-counter(counter1)  = value 10
Sproc-counter(instances) = counter1 {}
Sproc-counter(lambda)    = {{increment 1}} {dict with ::::Sproc-counter([list [namespace tail [lindex [info level 0] 0]]]) {incr value $increment}}
Sproc-counter(next)      = 2
Sproc-counter(statics)   = value 0
% # Demonstrate sproc deletion
% rename counter ""
% parray Sproc-counter
"Sproc-counter" isn't an array
% $q
invalid command name "counter1"

Limitations

No arrays

Static variables cannot be arrays. Use dicts instead. This makes it impossible to create traces or upvar aliases on individual elements of an array-like static variable.

Limited traces

Traces can be set on static variables, but they have to be set inside the sproc script body. In other words, they have to be recreated every time the sproc instance is invoked. They will not detect access to the static variables outside of the sproc script body, e.g. by directly modifying the internal data structure.

No renaming

Renaming sprocs or sproc instances is not supported. The only allowed use of [rename] is to delete sprocs and sproc instances.


Techniques used in implementation

AMG: I wanted to avoid customizing each sproc instance's script body, so I needed some other way of communicating its unique ID into the (generic) script body. Since I want this code to be easy to use, this must be done automatically. I guess I could have used [interp alias] to curry an ID argument into the invocation, but I didn't want to confuse [info args]. Instead I took advantage of the proc name being a sort of hidden parameter, which is accessed using [lindex [info level 0] 0]. In the case of the sproc (the procedure that creates instances), this gives the name of the sproc. In the case of the instances themselves, this gives the instance name, and the sproc name was already compiled into the lambda and doesn't need to be looked up dynamically.

To make accessing the sproc or instance data easier, I used [upvar #0] to link a global variable to a local variable simply named "data".

I didn't want to generate any procs not intended to be called by the user of this code, so I instead used [apply] to do the [trace] scripts.

I did my best to facilitate bytecode sharing between all sprocs or between all instances of any given sproc. To do the former, I wrote generic code that dynamically figures out the sproc name using the trick described above. To do the latter, I generate one lambda to be used for all instances of a sproc, again dynamically figuring out the instance name. The name works somewhat like a "this" pointer in C++.

I was disappointed to discover that [info level 0] is not fooled by [interp alias], that it gives the real target proc name not the alias. If not for this, I would have simply made a generic instance proc and instantiated the sproc by creating aliases. But so it turned out, I had to make a separate proc for each instance. (This is better anyway because it enables deletion through rename instance "".) I wanted for the separate procs to share bytecode, so I wrote proc $nameid {*}$data(lambda).

To support deleting all instances when the sproc is deleted, I must maintain a list of instances. But I didn't want to write code to search-and-destroy through the instance list on instance deletion, so I used [dict unset] on a dict with empty string for the values. In this way I make a set using a dict.


Discussion

AMG: Do all sproc instances really share the same bytecode-compiled script body? Somebody please confirm this for me.


AMG: How well does this code work with namespaces?


[Category ???]