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 }
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 }