C-like syntax for numbers

Summary

slebetman: This implements what I'd call C-like syntax for numbers in Tcl. Basically, this means that not only can you use infix assignment like x = $y * 2 but you also need to declare the variables before using them. There are other implementations of this available somewhere in this wiki, mostly by Richard Suchenwirth. Prompted by GWM, I thought I'd dig up my very simple implementation which have been floating around usenet (google "syntax expressiveness"). Anyway, here's the code:

proc cleanupVar {name1 name2 op} {
    if {![uplevel 1 [list info exists $name1]]} {
        rename $name1 {}
    }
}

proc var {name {= =} args} {
    upvar 1 $name x
    if {[llength $args]} {
        set x [expr $args]
    } else {
        set x {}
    }
    proc $name args [subst -nocommands {
        upvar 1 $name $name
        if {[llength \$args]} {
            set $name [expr [lrange \$args 1 end]]
        } else {
            return $$name
        }
    }]
    uplevel 1 [list trace add variable $name unset cleanupVar]
}

The following is an example of how to use var:

proc test {} {
    var x
    var y = 10
    x = $y*2
    return $x
}
puts [test]

Another feature is that the variables actually exists in local scope even though their associated commands exists in global scope. This means that the variables can be used recursively:

proc recursiveTest {x} {
    var y = $x - 1

    if {$y > 0} {
        recursiveTest $y
    }
    puts $y
}
recursiveTest 10

should output the numbers 0 to 9. Another test:

proc test2 {} {
    var x = 10
    puts "this x belongs to test2 = $x"
}

proc test3 {} {
    var x = 100
    test2
    x = $x + 1
    puts "this x belongs to test3 = $x"
}

test3

output:

this x belongs to test2 = 10
this x belongs to test3 = 101

Larry Smith: All of this would be so much tidier if it were possible to declare local procs.

escargo 2007-02-04 - Might it make more sense for the default value of a declare var to be 0 (or, since these are supposed to be reals, 0.0)?

slebetman: I prefer a default value that is not a number. If you want to initialise it to 0 then declare it as:

var x = 0

Besides, these aren't supposed to be reals. They're regular Tcl variables with expr built-in. It's important to remember that because:

var x = 1 / 2

is not 0.5 but 0.

NEM: A few comments. Firstly, the advice to always brace your expr-essions applies here too:

% var x = "foo" eq "bar"
invalid bareword "foo"
in expression "foo eq bar";
should be "$foo" or "{foo}" or "foo(...)" or ...

The other performance and precision benefits also apply, so you should really write:

var x = {"foo" eq "bar"}

However, this doesn't actually work as "args" applies more quoting, resulting in x being assigned the literal string {"foo" eq "bar"}. It will also go wrong with any variable references. We can fix this with an uplevel, and we can also simplify the var proc considerably using interp aliases and setting the trace through the upvar alias:

proc var {name {= =} args} {
    upvar 1 $name x
    if {[llength $args]} { set x [uplevel 1 [linsert $args 0 expr]] }
    interp alias {} $name {} ::var $name
    trace add variable x unset cleanupVar
    return $x
}

slebetman: notes that performance and precision benefits can also be achieved by using it like:

x = \$x + \$y

but may be considered by some (myself included) as ugly.

Lars H: It's probably unnecessary to recreate the alias every time the variable is set, but indeed this is the kind of command that should be an alias. I think both variants are buggy with respect to recursion however, as it wants to remove the command every time a variable by that name is unset. Try changing test3 to

proc test3 {} {
    var x = 100
    uplevel {test2}
    x = $x + 1
    puts "this x belongs to test3 = $x"
}

to see the problem:

this x belongs to test2 = 10
Error: invalid command name "x"

Checking for another variable by the same name in the current context simply isn't good enough. Another implementation of var which adresses this bug (by only attaching an unset trace to the variable for which the command had to be created) is

proc var_set {name {= =} args} {
    uplevel 1 [list ::set $name [uplevel 1 [linsert $args 0 ::expr]]]
}
proc var {name {= =} args} {
    upvar 1 $name x
    if {[llength $args]} { set x [uplevel 1 [linsert $args 0 ::expr]] }
    set cmd [uplevel 1 {::namespace current}]::$name
    if {![llength [interp alias {} $cmd]]} then {
       interp alias {} $cmd {} ::var_set $name
       trace add variable x unset "[list ::interp alias {} $cmd {}];#"
    }
    return $x
}

It is possible to break it by upleveling an unset for the outer variable, but as long as unsetting happens because procedures return it should be OK.

slebetman: Note that unsetting the "local" should be OK. Just don't unset the variable of the same name at uplevel 1.

See Also

infix
Gadgets
Radical Language Modification
Let unknown know
A little math language
L