SIMEL
SIMEL

Reputation: 8939

strange behaviour of `eval` in Tcl

I have encountered a strange behaviour by the eval command that I can't understand. When I try use eval to run a command whose name is stored in a variable I get strange results.

The array mCallBackCont has the value ::postLayRep1||mainTableView sendToLoads under insert, so that*:

>>set mCallBackCont(insert)
::postLayRep1||mainTableView sendToLoads

Where ::postLayRep1||mainTableView is an object of a class that has a sendToLoads public method

When I try to do one of the following:

eval {::postLayRep1||mainTableView sendToLoads}
eval ::postLayRep1||mainTableView sendToLoads
eval "::postLayRep1||mainTableView sendToLoads"
eval $mCallBackCont(insert)
eval "mCallBackCont(insert)"

I get the correct behaviour, but When I use

eval {$mCallBackCont(insert)}

I get the error:

invalid command name "::postLayRep1||mainTableView sendToLoads"

When I try the same with a regular proc with no argument:

>>proc test_proc {} {return}
>>set a test_proc
>>eval {$a}

Everything works, but when I add argument, the same happens:

>>proc test_proc {val} {puts $val}
>>set a [list test_proc 1]
test_proc 1
>>eval {$a}
invalid command name "test_proc 1"

Since the eval command is part of a code from a library I'm using I can't change it, the only thing I can determine is the value of mCallBackCont(insert). The code in the library is:

if { [catch {eval {$mCallBackCont(insert) [namespace tail $this] $type $name $n $redraw}} e] } {
          error "Wrong number of arguments for the procedure \"$mCallBackCont(insert)\". Should be \"table type name num redraw\"."
}

* - I have tried to put the value in mCallBackCont(insert) both a list and as one string surrounded by ""

Upvotes: 1

Views: 290

Answers (1)

Donal Fellows
Donal Fellows

Reputation: 137787

First off, Tcl command names can have spaces in. Or in fact nearly any other character; the only ones you have to be careful of are :s because :: is a namespace separator, and a leading :: is fine as it just means that it is a fully-qualified name.

Because of that, ::postLayRep1||mainTableView sendToLoads is a legal but unusual command name. If you put the name in a variable, you can then use a read from that variable just as if it is a command name:

$theVariableContainingTheCommandName

Using an array element is nothing special in this regard.

Now, if you want to instead treat that as a script, you pass it to eval like this:

eval $theVariableContainingTheScript

The real problem you're having is that you instead are doing:

eval {$theVariableContainingTheScript}

That is defined to be exactly identical to doing just:

$theVariableContainingTheScript

That won't ever do what you appear to want. Looking at the code that's causing you the problem:

if { [catch {eval {$mCallBackCont(insert) [namespace tail $this] $type $name $n $redraw}} e] } {
    error "Wrong number of arguments for the procedure \"$mCallBackCont(insert)\". Should be \"table type name num redraw\"."
}

In this case, the value in the variable has to be the name of a command, and not just a script fragment. The simplest fix for you is to create an alias which binds in the extra arguments:

interp alias {} callBackForInsert {} ::postLayRep1||mainTableView sendToLoads

Then you can use callBackForInsert just as if it was the combination of calling ::postLayRep1||mainTableView with the first argument sendToLoads. It's effectively a named partial application. Alternatively, you can use a helper procedure:

proc callBackForInsert args {
    return [uplevel 1 {::postLayRep1||mainTableView sendToLoads} $args]
}

But that's both uglier and slower in this simple case. Better in 8.6 is to use tailcall:

proc callBackForInsert args {
    tailcall ::postLayRep1||mainTableView sendToLoads {*}$args
}

But that is still slower than an alias because of the overhead of the extra stack frame manipulation.


However, the nicest fix if you can is to alter the library so it uses the callback like this (assuming Tcl 8.5 or later):

if { [catch {eval {{*}$mCallBackCont(insert) [namespace tail $this] $type $name $n $redraw}} e] } {
    error "Wrong number of arguments for the procedure \"$mCallBackCont(insert)\". Should be \"table type name num redraw\"."
}

Which can be simplified to:

if { [catch {{*}$mCallBackCont(insert) [namespace tail $this] $type $name $n $redraw} e] } {
    error "Wrong number of arguments for the procedure \"$mCallBackCont(insert)\". Should be \"table type name num redraw\"."
}

A good rule of thumb is that there's hardly ever a reason to use eval in modern Tcl code; a {*}-expansion is virtually always closer to what is intended.

If you're stuck with 8.4 but can change the library code, you can do this instead:

if { [catch {eval $mCallBackCont(insert) {[namespace tail $this] $type $name $n $redraw}} e] } {
    error "Wrong number of arguments for the procedure \"$mCallBackCont(insert)\". Should be \"table type name num redraw\"."
}

This uses the fact that eval will concatenate its arguments before feeding them back through the Tcl script evaluation engine.


The combination of aliases, expansion, tailcall and (not used in this answer) ensembles lets you do awesome stuff with very little code, allowing complicated remixing of arguments without having to load yourself down with lots of helper procedures.

Upvotes: 4

Related Questions