Version 69 of set

Updated 2015-02-16 16:24:31 by dbohdan

set, a built-in command, reads and writes variables.

Synopsis

set varName ?value?

Documentation

set

See Also

array
?set
expr
trace
unset
take
foreach
often used to set multiple variables in one stroke

Description

A variable a value that is bound to, and accessd by a name. A variable is also bound to a particular namespace. set returns the value of variable varName. varName may be any value. If value is specified, then set the value of varName to value, creating a new variable if one doesn't already exist, and return its value. If varName contains an open parenthesis and ends with a close parenthesis, then it refers to an array element: the characters before the first open parenthesis are the name of the array, and the characters between the parentheses are the index within the array. Otherwise varName refers to a scalar variable.

If varName is fully-qualified (begins with :: and includes all containing namespaces), the variable in the specified namespace is read or written. If varName is unqualified, the following applies: If varName can be resolved to an existing variable relative to the current namespace, it resolves to that variable. Otherwise, an attempt is made to resolve varName relative to the global namespace. If that fails, varName is resolved relative to the current namespace.

If no procedure is active, then varName refers to a namespace variable, which may be in the global namespace. If a procedure is active, then varName refers to a parameter or local variable of the procedure unless otherwise specified by global, variable], or upvar.

set can entirely replace variable expansion.

Basic Examples

set greeting hello
set greeting ;# ->hello
set person(name) bob
set person(name) ;#-> bob
set (name) bob ;# the is an array variable, where the array name is the empty string
set (name) ;#-> bob
set {} hello 
set {} ;#->hello

Variable Resolution Caveat

ulis 2003-11-16:

Try this:

set ::version 1.0
namespace eval ns {
    set version 0.9
}
puts $::version
catch {puts $ns::version} msg
puts $msg

Result:

0.9
can't read "ns::version": no such variable

Explanation:

As stated in the Tcl manual: if the name does not start with a :: (i.e., is relative), Tcl follows a fixed rule for looking it up: Command and variable names are always resolved by looking first in the current namespace, and then in the global namespace.

In the above script the variable version wasn't defined inside the namespace so Tcl used the existing global variable.

To avoid that, always declare namespace variables with the variable command:

set ::version 1.0
namespace eval ns {
    variable version 0.9
}
puts $::version
catch { puts $ns::version } msg
puts $msg

New result:

1.0
0.9

I (ulis) think that it would be better if the search in the global space was used when refering a variable and avoided when setting a variable.

Setting Multiple Variables at Once

MSW: For those who dislike doing multiple assignments at once with foreach in the style

foreach {a b c} {1 2 3} {}

and who don't want to use lassign, here is a multiple argument set (with help from RS):

if {[info procs tcl::set]=={}} then {rename set tcl::set}
proc set {args} {
    switch [llength $args] {
        0 {return -code error {wrong # args: should be set varname ?newvalue? ?varname ?newvalue?? ...}}
        1 {return [uplevel [list tcl::set [lindex $args 0]]]}
        2 {return [uplevel [list tcl::set [lindex $args 0] [lindex $args 1]]]}
        default {
            uplevel [list tcl::set [lindex $args 0] [lindex $args 1]]
            return [uplevel [list set [lrange $args 2 end]]]
        }
    }
}

Use like this

% set a 1 b 2 c 3
=> 3
% set d 15 e [expr int(100*rand())] c
=> 3
% list $a $b $c $d
=> 1 2 3 15

Duoas: The foreach..break idiom is so prevalent in Tcl, and so common, that experienced Tcler's automatically recognize it as a set replacement idiom:

set ls [list 1 2 3]
foreach {var1 var2 ...} $ls break

However, something about it has always bothered me: I just dislike programming to the side-effects. I've submitted TIP #58 to extend set such that it can assign to multiple variables, but not as above, where the values to assign are interleaved with the variable names. Usually the values come from a list and the above implementation would require zipping variable names and values together before use, then evaling or expanding. It doesn't obviate the need to use that silly foreach..break idiom.

Littered throughout my own code is the use of this simple little routine:

proc sets args {
    set names [lrange $args 0 end-1]
    set values [lindex $args end]
    uplevel 1 [list foreach $names $values break]
    return [lrange $values [llength $names] end]
}

And an example of use:

sets x0 y0 x1 y1 [.canvas coords my-rectangle-tag]

This is much more Tclish and intuitive. Note also that you can get what is not used for later use (foreach requires you use it now or not at all):

set ls [sets a b $ls]
# do something with $a and $b, and maybe sometime later with the rest of $ls

A more concrete example:

% set ls [sets a b {1 2 3 4 5}]
3 4 5
% puts $ls
3 4 5
% puts $b
2

As per my TIP submission, set is easily extended to have such functionality without slowing it down when used as per the current specification (well, except one or two processor instructions when errors occur). When used in the extended form it is faster than using foreach, which has a lot of extra stuff to handle multiple, concurrent lists.


MJ: in 8.5 we have lassign, which is set with the arguments reversed. The example above then translates to:

% set ls [lassign {1 2 3 4 5} a b]
3 4 5
% puts $ls
3 4 5
% puts $b
2

Duoas: Me feels stupid for having missed that... I learned Tcl moving into 8.0 and I'm still a little behind in a lot of 8.5 improvements.

MJ: No need to feel stupid, Tcl 8.5 has a lot of new goodies, see Changes in Tcl/Tk 8.5.

Double Indirection

See also: An Essay on Tcl Dereferencing

In some languages, notably PHP, an additional dollar sign can be added to a variable to achieve double-indirection. e.g. $$var. Tcl doesn't support such syntax, but set can be used to the same effect:

% puts [set $var] ;# This works safely
5

The following example, which uses set instead of $, is equivalent:

% puts [set [set var]] ;# as does this
5

Similarly, to print the values of var1, var2, and var3:

set var1 3.14159
set var2 hello
set var3 13
foreach num {1 2 3} {
    puts "var$num = [set var$num]"
}

output:

var1 = 3.14159
var2 = hello
var3 = 13

upvar can also provides access to to other variables, even when they are in the same scope:

set var1 hello
upvar 0 var1 var2

eval could also be used to achieve double indirection (but there are major caveats):

% set a 5
5
% set var a
a
% puts $$var              ;# This doesn't work
$a
% eval puts $$var         ;# This does  - but it's dangerous
5

One caveat is that if $var has a value containing any special characters (e.g. whitespace, semicolon), they'll get interpreted, and where this is inadvertent, could result in an error or an exploit.

An Alternative to return

proc returns its last evaluated result, so it's a common idiom to use set instead of return as last command.

set res
return $res

Some Tcl style guides recommend using the explicit return alternative. The rationale is is that using an explicit return guards against inadvertantly adding addtional code after the set command.

Before return got byte-compiled (i.e., before Tcl 8.4), the set idiom was faster than the return idiom for returning a variable value. This is no-longer true.

A Verbose set

RS:

As Tcl has no reserved words, you can even write your own set command (make sure its effects are like the original, or most Tcl code might break). For instance, this version reports its actions on stdout:

rename set _set
proc set {var args} {
    puts [list set $var $args]
    uplevel 1 _set $var $args
}

This might help in finding what was going on before a crash. When sourced in a wish app, shows what's up on the Tcl side of Tk (as long as you can find the program's stdout).

Bug: array and $

As of Tcl 8.6.3, set myarray($) one results in an array variable named the empty string because set interprets $, even at the end of index, as variable substitution, and removes it. Meanwhile, variable substitution handles the same syntax just fine:

% set a($) one
one
% puts $a($)
can't read "a($)": no such element in array
% set {a($)} two
two
% puts $a($)
two
% 

Why C Programmers Hate Tcl Variable Scope Resolution!

JoGusto: Yes, that is meant to be provocative. But, it is a serious problem for software developers who have spent years, or decades, programming in languages such as C, where a global variable is just that: GLOBAL!

It is absolutely NOT intuitive to the average C programmer that all global variables disappear from scope (YES! Ridiculous!) when you enter a proc... in C, a global remains in scope, with no special syntax required, unless that global name is hidden because of a more enclosed declaration inside a block. Even then, the global is accessible (in C++ at least) via the same notation Tcl uses: the double colon.

This scope resolution problem is just really abhorrent to those of us who "Think in C" and try valiantly to script in Tcl, only to repeatedly bump up against that old bugaboo: "I forgot the double colon! My global variable is "undefined"" It is ridiculous to have to repeatedly declare variables that are global within the local scope just to access them.

Yea, I know it's way too late to change, but its a common complaint I get from many people about Tcl, and Python, that "they got the whole global variable thing WRONG..." and it's definitely something that trips up a lot of people who are not used to it.

dbohdan 2015-02-16: OTOH, having to explicitly declare your globals prevents accidental mutation of global state. This is important because in Tcl unlike in C variables can be set without prior declaration and the compiler won't catch a redeclared variable for you.