static variables

Richard Suchenwirth 2002-05-05 - Static variables are seen here as variables that are local to a procedure, but retain their value ("state") between calls to the procedure. One way to maintain a variable's state is to make it global (which includes putting it into a namespace), but this is frowned upon as it clutters the environment. Another way, as shown in intgen in Braintwisters, is to store the value as default for a proc argument, and rewrite the proc definition. To generalize the specific approach taken there, we first write a proc that rewrites one argument's default with a given value:

 proc redefaultProc {procn argn value} {
    set argl {}
    foreach arg [info args $procn] {
        if [info default $procn $arg default] {
            if {$arg==$argn} {set default $value}
            lappend argl [list $arg $default]
        } else {
            lappend argl $arg
        }
    }
    proc $procn $argl [info body $procn]
 }

With this tool in hand, we can proceed to write static, a command that somehow resembles a C "declaration", but actively registers arguments as "static", i.e. installs a write trace so every time the variable is changed, the proc definition is rewritten (for performance, don't assign to it too often!):

 proc static args {
    set caller [lindex [info level -1] 0]
    foreach arg $args {
        uplevel 1 [list trace add variable $arg write [list staticTrace $caller]]
    }
 }
 # ... and the trace proc, which only serves to ignore 'el' and 'op':
 proc staticTrace {caller name el op} {
    upvar 1 $name var
    redefaultProc $caller $name $var 
 }

..and now testing the whole thing. Note that a static variable needs also to be written into the proc argument list with a default value to start with:

 proc x {y {z 0}} {
    static z
    puts [incr z]:$y
 }
 % x hello
 1:hello
 % x world
 2:world

Looks like we made it - proc x has a "counter" z that remembers how often it was called before, but you can reset it explicitly (as the call to x pre-increments, use -1 to start next time with a 0 counter):

 % x reset -1
 0:reset
 % x test
 1:test

Static variables need not be numbers, for instance this example shows that it always remembers the previous call's argument (any string):

 proc lookback {now {before {}}} {
     static before
     puts $before->$now
     set before $now
 }

And intgen can now be rewritten like this:

 proc intgen {{seed 0}} {static seed; incr seed}

Ulrich Schoebel 2002-10-09 - Another way to implement static variables without a need for traces is to use a reserved namespace (in this example ::StaticVariables) for their storage. This handles scalar and array variables. The variable names must not contain namespace qualifiers (why should they?).

 proc static {args} {
      set ns ::StaticVariables
      append ns [string trimright [uplevel 1 namespace current] :]
      append ns :: [namespace tail [lindex [info level -1] 0]]
      foreach var $args {
         if {![string equal [namespace tail $var] $var]} {
            return -code error -errorinfo "Static variable $var has namespace qualifiers"
            }
 #       if {![info exists ${ns}::$var]} {
            namespace eval $ns [list variable $var] ;# (1)
 #       }
         uplevel 1 upvar #0 ${ns}::$var $var
         }
      }

This proc is completely sufficient as long as you don't rename the proc using static variables. If you need renaming, add the following:

 rename rename _rename

 proc rename {old new} {
      set ns ::StaticVariables
      append ns [string trimright [uplevel 1 namespace current] :]
      append nso $ns :: [string trimleft $old :]
      if {[string length $new] == 0} {
         foreach var [info vars ${nso}::*] {
            catch {unset $var}
            }
         _rename $old ""
         return ""
         }
      append nsn $ns :: [string trimleft $new :]
      foreach var [info vars ${nso}::*] {
         if {![catch {set $var} value]} {
            namespace eval $nsn [list set [namespace tail $var] $value]
            } elseif {[array exists $var]} {
            namespace eval $nsn [list array set [namespace tail $var] [array get $var]]
            }
         catch {unset $var}
         }
      _rename $old $new
      }

US


RS: A good alternative! But I think the existence check marked at (1) above is not needed; redeclaring an existing variable can't hurt.

US: I agree. Haven't read the man page thoroughly enough. Commented check.

RHS The following works also:

 namespace eval ::localvars {}
 proc static {varName args} {
    if {![string equal [namespace tail $varName] $varName]} {
        return -code error \
            "static variables may not contain namespace qualifiers"
    }

    set procName [lindex [info level -1] 0]
    namespace eval ::localvars [list variable [list $procName $varName]]
    uplevel 1 [list upvar ::localvars::[list $procName $varName] $varName]

    switch -exact -- [llength $args] {
        0 { }
        1 {
            set ::localvars::[list $procName $varName] [lindex $args 0]
        }
        default {
            return -code error \
                "wrong # args: should be \"local varName ?value\""
        }
    }

    trace add command $procName delete \
        [list ::localvars::renameCallback $varName]

    trace add command $procName rename \
        [list ::localvars::renameCallback $varName]
 }

 proc ::localvars::renameCallback {var oldname newname op} {
    switch -exact -- $op {
        delete {
            unset ::localvars::[list $oldname $var]
        }
        rename {
            set old ::localvars::[list $oldname $var]
            set new ::localvars::[list $newname $var]
            set $new [set $old]
            unset $old
        }
        default {
            error "Unexpected op $op to localnames::renameCallback"
        }
    }
 }

The code and tests for the above can be found at http://robert.rkseeger.net/tcl


Zarutian: 10. may 2005: an other way would be putting a execute trace on the procedure which updates the defaults to the procedure.

Zarutian: 6. june 2005: sorry I have been a little bit busy.

 package require Tcl 8.4
 proc proc_with_staticVars {name args staticVars body} {
   set staticVarsWithValues [list]
   foreach item $staticVars {
     if {[llength $item] == 2} {
       lappend staticVarsWithValues $item
     } elseif {[llength $item] == 1} {
       upvar $item tmp
       lappend staticVarsWithValues [list $item $tmp]
     }
   }
   set old_args $args
   foreach item $staticVarsWithValues {
     lappend args $item
   }
   lappend args [list old_args $args]
   proc $name $args $body
   trace add execution $name leave saveStaticVars
 }
 proc saveStaticVars args {
   set op [lindex $args end]
   set command_string [lindex $args 0]
   set proc_name [lindex $command_string 0]

   if {$op == "leave"} {
     set vars [uplevel 1 [list info locals]]
     set varsAndValues [list]
     set old_args {}
     foreach item $vars {
       upvar $item tmp
       if {$item == "old_args"} {
         set old_args $tmp
       } else {
         lappend varsAndValues [list $item $tmp]
       }
     }
     set args [list]
     foreach item $old_args { lappend args $item }
     foreach item $varsAndValues { lappend args $item }
     proc $proc_name $args [info body $proc_name]
   }
 }

Should work now

Zarutian 2. june 2008: but it doesnt as saveStaticVars gets invoked after the command it is trace has returned and destoryed its callframe.


RS 2005-05-11: Here's how I would reimplement the namespace-based solution these days - rename at your own risk (statics will be reset to initial values then):

 namespace eval ::static {}
 proc static {name {value ""}} {
    set caller [lindex [info level -1] 0]
    namespace eval ::static::$caller {}
    set qname ::static::${caller}::$name
    if {![info exists $qname]} {set $qname $value}
    uplevel 1 [list upvar #0 $qname $name]
 }
#-- Testing:
 proc intgen {} {
    static i 0
    incr i
 }
 % intgen
 1
 % intgen
 2
 % intgen
 3

See Streams for many usage examples of static variables (and another intgen variation) - Closures - static data in command procedures


Inspired by the use of namespaces in jimulation by RS here is another version of the namespace-based solution (and another intgen variation) posted by kruzalex

 proc static {name {value 0}} {
    set caller [lindex [info level -1] 0]
    set qname ::static::${caller}
    if {![info exists ${qname}::$name]} {
        foreach var [list [lrange [info level 0] 1 end]] {  
        if {[llength $var]==1} {lappend var $value}  
        namespace eval $qname [linsert $var 0 variable]
        }
    }
    uplevel 1 [list upvar 0 ${qname}::$name $name]
 }

#-- Testing:
 proc intgen {} {
    static i 0
    incr i
 }
 % intgen
 1
 % intgen
 2
 % intgen
 3

AMG: See sproc for my implementation of static variables.


RLE gave a very nice example of pseudo-statics on Playing Scheme:

proc intgen {{i 0}} {
  proc intgen "{i [incr i]}" [info body intgen]
  return $i
}