0
votes

Sorry for the title couldn't think of anything better.

Heres my question:

Im trying to change a variable inside a proc only when the user hits space. the proc loops itself with after so if the user wish to hit space more then once the variable will increment.

Heres what I know:

There are many ways to go about this. You can pass variable inside the proc, you can link the variable with global or upvar and/or if your in a namespace then you can use variable. but the only one that seem to work with my is global. I'm getting a feeling it's because global makes a link but if thats true then variable should work too, right?

Here my test code:

proc test1 {} {
    global testing
    bind . <Key-a> {incr testing}
    puts $testing
    puts "test2"
    after 100 test2
}


namespace eval test2 {
    variable testing 0
    namespace export {[a-z]*}
    proc of1 {} {
            variable testing
            bind . <Key-a> {incr testing}
            puts $testing
            after 100 test3::of1
    }
}


proc test3 {testing} {
     bind . <Key-a> {incr testing}
     puts $testing
     puts "test4"
     after 100 test4 $testing
 } 
set testing 0
#test1 
test2::of1
#test3 0
grid .c 

side question:

Why is it that in the global namespace we use set and global while in namespace we use variable (that seem to set and do global in one command). they seem to do the same job in different namespaces?

1
You've got one key problem: bind evaluates binding scripts in the global scope. - Donal Fellows

1 Answers

1
votes

Variables in Callbacks

The scripts registered by the bind command — also in things like after events and fileevent callbacks — are evaluated in the global scope because they may be called long after the procedure that defined them returns; Tcl does not do scope capture (and that's actually a really complicated feature, so it isn't likely to come soon unless someone writes lots of code). This means that the variable that you want your procedure to notice changes to must also have global scope.

However, namespaced variables count just fine as global variables for the purpose of this discussion as they're nameable from a global context (real local variables are not). That means that we can do several ways to build a script that accesses a namespace variable from a bind-defined callback. Here's one of the nicer ones:

bind . <Key-a> [namespace code {incr testing}]

That's effectively the same as this:

bind . <Key-a> [list namespace eval [namespace current] {incr testing}]

(There are some strict differences that don't matter in this example.)

Another way to do the callback is this:

bind . <Key-a> [list incr [namespace which -variable testing]]

Which in this case is going to be much like:

bind . <Key-a> [list incr [namespace current]::testing]

If things are getting any more complicated than this toy example, it's time to stop updating variables directly in a binding script and instead write a helper procedure. That always simplifies things a lot. Or to use a class/object to encapsulate the details.


The variable Command: Why and Where to Use It

Why is it that in the global namespace we use set and global while in namespace we use variable (that seem to set and do global in one command). they seem to do the same job in different namespaces?

That's a good question. In fact, what global does is very much like upvar #0 (with the variable names doubled up), and set is a fundamental variable access command. They're commands that you can use regularly wherever you want their behaviour.

The variable command is rather stranger. What it does is three-fold:

  1. If called in a namespace context and the variable does not exist in that namespace, it creates that variable in a present-but-unset state.
  2. If called in a context with local variables, it links a local variable with the name (after stripping everything up to the last namespace separator) to a namespace variable with the name (using the whole supplied name if there are qualifiers, and resolving non-absolute names with respect to the current context namespace). This also forces the namespace variable to exist in the present-but-unset state.
  3. If a value is given, the namespace variable is set to the value. This gets rid of the present-but-unset-ness.

The important behaviour is actually the creating of that present-but-unset state, since otherwise you can end up with a set (or array set) in the namespace escaping that namespace and instead working with a global variable, but not always. It all depends on the exact behaviour of the code that resolves variables, and that's deeply tricky. This is hard to explain properly, and ridiculously hard to justify. It's been the cause of quite a few outright bugs, and is definitely no better than a horrible misfeature.

The setting of the initial value is little more than a lollipop; you could instead put set straight afterwards without ill-effect. What's more, it inhibits using variable to pull in multiple variables from a namespace except in the case where you're setting them to a known value; great for initialisation, but sucky for other uses. (In case you hadn't guessed, I think this is an area of Tcl where the interface was got rather badly wrong back when it was introduced, back in Tcl 8.0. Not one bit of this is nice.)

The key take-away is this though:

  • Always use variable in your namespaces to declare variables as this is the only way you can be sure that the semantics is predictable. You can then initialise them any way you want. (You have to do it this way if you're creating arrays.)

Fully-qualified variable names have no craziness associated with them. Tcl always knows exactly how to look up the thing you're naming in that case.