Version 2 of Traces

Updated 2000-08-03 20:50:44

Hmm. Let's set the ball rolling...


A simple way to get a read-only global variable is to use a suitable trace:

     set ROglobal someValue
     trace variable ROglobal w "[list set ::ROglobal $ROglobal];error read-only;#"

Notice the time- and hair-saving tricks:

  1. Using [list] to construct a guaranteed safe command for later execution.
  2. Using colon notation to force a reference to a global variable, whatever the context.
  3. Inserting the global name of the variable in the trace command, instead of working with its local referent.
  4. Using a trailing ";#" to trim the undesirable extra arguments from the trace command.

DKF

This is great code, but hard to find if you're thinking of a constant or constants or C's #define. (There! Now the Wiki search will find this page!) - RWT


Neat... I remember having had trouble when the trace was set on ::var, yet usage was based on global var, or the other way around. Does anyone know what the exact behavior is when mixing these two approaches?

JCG

The thing to remember is that traces are always called in the context of the operation that is performing the read, write or unset, and the standard parameters passed in from the Tcl system refer to the way in which the variable was referred to in that context. Whenever you are setting up a trace on a variable that exists independently of the stack (i.e. a global or namespace variable - the two are really the same thing anyway) then it is much less hassle to pass a fixed name for that variable in as one of your own parameters to the trace (you can sort-of form closures in Tcl using appropriate scripts, and some of the more cunning lambda-evaluation schemes I've seen have been based on this.) The only time this is not going to work (in standard Tcl) is when you are dealing with variables that only exist on the call-stack - procedure locals. With these, you have to carefully build an [upvar] reference to the variable and then use that name to figure out what is going on. This is not easy, but it has been adequately covered in many books, like BOOK Tcl and the Tk Toolkit which is where I learnt all this stuff (if I can remember that far back!)

DKF


Donal, you might be talking about the following:

    trace variable ::var w monitor 
    proc monitor {name args} {
            upvar $name value
            puts "'[info level 1]' changes '$name' to '$value'"
    }

    ### Change 1
    proc change1 {} {
            set ::var bar
    }
    change1

    ### Change 2
    proc change2 {} {
            global var
            set var foo
    }
    change2

    ### Change 3
    proc change3 {} {
            upvar var local
            set local bar
    }
    change3

The above code produces the following output:

    'change1' changes '::var' to 'bar'
    'change2' changes 'var' to 'foo'
    'change3' changes 'local' to 'bar'

The problem is that there is no way of knowing which variable is actually being modified, all one has access to is its alias name as used in the set command which trigers the trace, i.e., in monitor the parameter name is equal to local, how do you resolve that name back to the original ::var variable?

-- JC

There is no standard mechanism for doing this. So I instead pass a globally-valid (fully-quantified) variable name as part of the trace script that I supply. Like that, I don't care about how the variable was accessed since I can always access it myself using a clearly defined route. And, because of the restrictions involved with the use of variables and Tk, I find sticking to only putting traces on globally-valid names to not be a problem.

Note that [namespace current] is very useful when you want to put in code to automatically figure out what the globally-valid variable name actually is.

DKF


If you want to set up a trace on an expression, then you implement this using the style advocated in the following example:

Suppose you want to have a trace on a pair of variables such that some callback (which I'll imaginatively describe as callback here) is called whenever some complex set of conditions (either $VAR_I>0 && $VAR_B=="true" or $VAR_I>=10 && $VAR_B=="false") then you would do it like this:

  trace variable VAR_I w doTest
  trace variable VAR_B w doTest
  proc doTest args {
      upvar #0 VAR_I i VAR_B b
      if {
          ($i>0 && [string equal $b "true"]) || 
          ($i>=10 && [string equal $b "false"])
      } then {
          upvar #0 callback
      }
  }

You can't extend this to a general watch of an expression, since an expression can include a call to a general command, and solving exactly what variables a general command accesses is tough. Even if we restricted ourselves to procedures, we would still need to solve the Halting Problem (a famously insoluble thing in Computer Science, where there is a proof of insolubility...) Since you can usually tell straight off what variables in an expression actually matter, you can shortcut all that stuff and just tell Tcl exactly what to watch yourself...

DKF


Come on people, this page isn't just "Ask Dr. Donal" - fill in more of this stuff yourselves!

DKF