scopetcl

Scope/context of anonymous functions/procedures


This is one of those novice questions that arise for those of us who don't really understand the way things work in the background and get by for awhile thinking we understand; and, then, when encountering something new, we realize we didn't really understand what we thought we did.

I'm trying to make a function that is a do-while loop (to iterate a linked list by key sequence) that can accept a variable that is the loop body. My question has to do with passing the body script as a variable to another function/procedure.

In Javascript, I just pass an anonymous function. In Tcl, the scoping is different; I don't know what it is called but it is not lexical scoping. If I try an anonymous procedure, the variables that are in the anonymous procedure but not in the one to which it is passed, are treated as new variables in that procedure's scope. Therefore, I tried the uplevel upvar approach shown in Prof. Ousterhout's text and it works. And, I think I even understand it. But it appears the reverse of the way I've thought of it in JavaScript.

This tiny example, illustrates what I mean. In JS, I've always thought that I was passing the anonymous function to the other function (callee?) and it was executed in its scope. But, in Tcl, that thinking doesn't seem to work; for it appears that a variable name is being passed to the callee procedure to provide a loop variable in the callee's scope back to the context of the caller procedure, and the anonymous procedure is execute in the context of the caller.

My question is, in both Tcl and JS, in these example, is the anonyProc being executed in the context of LoopBody or is it really executed in the context of Complex and the loop counter from LoopBody is made available to it? Is there really a difference in what takes place in the code that processes the script or is it just two ways of writing the same thing?

I've one additional question specfic to Tcl, which is, Can this be done with an anonymous procedure and using apply? My first attempt is at the very end and the variable value is in the context of LoopBody rather than Complex.

Thank you for considering my question.

proc LoopBody {upvarName startKey endKey anonyProc } {
  upvar #1 $upvarName i
  for { set i $startKey } { $i < $endKey } { incr i 1 } {
    uplevel $anonyProc
  } 
}
proc Complex {} {
  foreach value {100 200 300} {
    LoopBody key 10 15\
        { 
          puts "key $key value [incr value $key]" 
        }
    puts ""
  } 
}
Complex

key 10 value 110
key 11 value 121
key 12 value 133
key 13 value 146
key 14 value 160

key 10 value 210
key 11 value 221
key 12 value 233
key 13 value 246
key 14 value 260

key 10 value 310
key 11 value 321
key 12 value 333
key 13 value 346
key 14 value 360

function loopBody (startKey, endKey, anonyProc) {
  for( let i = startKey;
        i < endKey;
        i++ ) {      
     anonyProc(i);
  }
}

function complex() {
  [100,200,300].forEach( (value,key) => {
     loopBody(
        10,
        15,
        (x) => {
          console.log(`x: ${x} value: ${value += x}`);
        }
     );
     console.log('');
  });     
}

complex();

Failed anonymous procedure attempt.

proc LoopBody {startKey endKey anonyProc } {
  for { set i  $startKey } { $i < $endKey } { incr i 1 } {
    apply $anonyProc $i
  }
}

proc Complex {} {
  foreach value {100 200 300} {
    LoopBody 10 15\
        { { x } {
            puts "x $x value [incr value $x]"
          }
        }
    puts ""
  }
}

Complex

x 10 value 10
x 11 value 11
x 12 value 12
x 13 value 13
x 14 value 14

x 10 value 10
x 11 value 11
x 12 value 12
x 13 value 13
x 14 value 14

x 10 value 10
x 11 value 11
x 12 value 12
x 13 value 13
x 14 value 14

Solution

  • In Tcl, you need to think in terms of positions on the stack of stack frames. That is what both uplevel and upvar work with. There is no capturing of variables (that can be simulated, but both really is doing copies — that incr wouldn't work — and is quite a bit more complicated than what you have here). Lambda terms (the things you pass to apply) are just like procedures, except that the way that you specify their current namespace is different; most code that uses them leaves that at default (which is the global namespace).

    Here's a version that works. (This happens to also be the one I would really recommend if you are going the route with lambda terms at all; this is reasonably efficient.)

    # it is a very good idea to ensure [uplevel] only gets one argument script
    # using [list] to build it is highly recommended and very efficient 
    proc LoopBody {startKey endKey anonyProc} {
        for {set i $startKey} {$i < $endKey} {incr i 1} {
            uplevel 1 [list apply $anonyProc $i]
        }
    }
    
    proc Complex {} {
        foreach value {100 200 300} {
            LoopBody 10 15 {{x} {
                upvar 1 value value
                puts "x $x value [incr value $x]"
            }}
            puts ""
        }
    }
    

    The simplest sort of lambda construction is done with a procedure like this:

    proc lambda {arguments body} {
        set ns [uplevel 1 {namespace current}]
        return [list $arguments $body $ns]
    }
    

    A more complex version would also generate the upvar or capture (read-only copy) for you, but at the same time would be less general. Without that extra complexity (and with lambda as above), you get this:

    proc LoopBody {startKey endKey anonyProc} {
        for {set i $startKey} {$i < $endKey} {incr i 1} {
            uplevel 1 [list apply $anonyProc $i]
        }
    }
    
    proc Complex {} {
        foreach value {100 200 300} {
            LoopBody 10 15 [lambda {x} {
                upvar 1 value value
                puts "x $x value [incr value $x]"
            }]
            puts ""
        }
    }
    

    but there literally isn't any benefit to it here; it is at least as easy to write the literal form.


    It is very rare for use of #1 in upvar/uplevel to be a good idea; it binds to an absolute level on the stack. Before coroutines, I'd never seen it. (It makes more sense in coroutines; that level can be treated as a useful place to store coroutine-specific variables.) In your code it is more of a bug waiting to happen.

    The actual normal way of writing this code would be:

    proc LoopBody {varName startKey endKey body} {
        upvar 1 $varName i
        for {set i $startKey} {$i < $endKey} {incr i 1} {
            uplevel 1 $body
        } 
    }
    proc Complex {} {
        foreach value {100 200 300} {
            LoopBody key 10 15 { 
                puts "key $key value [incr value $key]" 
            }
            puts ""
        } 
    }