Tcl Part I · Module 11 · Scope and Namespaces
upvar: Operating on the Caller's Variable
Two earlier chapters stopped mid-sentence and pointed here. Chapter 7.1 showed that an array cannot be handed to a procedure, because an array is not a value, and said the way round it was to pass a name. Chapter 10.8 showed a procedure filling in its caller's variables and deferred the mechanism. That mechanism is upvar: it takes the name of a variable in another frame and makes a local name refer to it, so the procedure reads and writes the caller's variable directly. It is the most powerful tool in this module and the one most worth using sparingly, and this chapter covers both halves of that.
Intermediate12 min readTclPart IScopeupvarArrays
Part I · Module 11 · Chapter 11.4 · upvar: Operating on the Caller's Variable
1. Passing a name instead of a value
proc push {listVar value} {
upvar 1 $listVar l
lappend l $value
}
set results {}
push results "ok"
push results "fail"
puts $resultsok failLook at the call: push results "ok" passes results bare, not $results. The procedure is given the name of a variable, and upvar 1 $listVar l makes its local l another name for that variable in the caller's frame. From then on, lappend l appends to the caller's list.
This is the same distinction Chapter 7.1 §6 drew: a value goes in as $x, a name goes in as x.
2. The level argument
upvar ?level? otherName localName ...
1— the caller's frame. This is what you almost always want, and it is the default if you leave the level out.#0— the global frame, counting from the bottom rather than relatively.0— the procedure's own frame, which makes a local alias for another local.
set g "global value"
proc lvl1 {name} { upvar 1 $name v ; return $v }
proc mid {} { set local "mid-local" ; return [lvl1 local] }
puts "upvar 1 : [mid]"
proc toglobal {name} { upvar #0 $name v ; return $v }
puts "upvar #0 : [toglobal g]"
proc nolevel {name} { upvar $name v ; return $v }
set q "q value"
puts "default : [nolevel q]"upvar 1 : mid-local
upvar #0 : global value
default : q valueDeeper levels count outward. From a procedure two calls down, uplevel's sibling numbering applies to upvar as well — 2 is the caller's caller:
proc outer_f {} { set mine "outer_f-var" ; middle_f }
proc middle_f {} { return [inner_f] }
proc inner_f {} {
return [list [uplevel 1 {info exists mine}] [uplevel 2 {info exists mine}]]
}
puts [outer_f]0 1Level 1 from inner_f is middle_f, which has no mine; level 2 is outer_f, which does.
3. It creates the variable if it is not there
The link is to a name, so the caller's variable need not exist yet:
proc setter {name val} {
upvar 1 $name v
set v $val
}
puts "before : [info exists brandnew]"
setter brandnew 7
puts "after : $brandnew"before : 0
after : 7And testing through the link before anything is written is safe — it reports absence rather than erroring:
proc peek {name} {
upvar 1 $name v
return [info exists v]
}
puts [peek never_set_anywhere]04. The same name-clash rule as global
Chapter 11.2 §3 showed global refusing a name already in use. upvar behaves identically:
set outer_var "the caller's"
proc clash {name} {
set v "local first"
upvar 1 $name v
return $v
}
clash outer_varvariable "v" already existsAnd, as before, the version that catches people is a parameter:
proc pclash {v} {
upvar 1 v v
return $v
}
pclash hellovariable "v" already exists5. Arrays: the promise from Module 7
Chapter 7.1 established that an array cannot be passed or returned, because it is not a value. upvar is the answer:
proc fill {arrName} {
upvar 1 $arrName a
set a(x) 1
set a(y) 2
}
fill myarr
puts "elements : [lsort [array names myarr]]"
puts "myarr(x) : $myarr(x)"elements : x y
myarr(x) : 1Reading one works the same way:
proc total_of {arrName} {
upvar 1 $arrName a
set t 0
foreach k [array names a] { incr t $a($k) }
return $t
}
array set counts {pass 4 fail 1 skip 2}
puts [total_of counts]7This is the only mechanism for it. Every procedure in existing Tcl that takes an array does it this way, which is why recognising upvar 1 $arrName a on sight is worth something.
6. It is a weaker contract than returning a value
Chapter 10.8 §7 made this argument and can now be stated precisely. Compare:
# returns a value
proc minmax {values} {
set s [lsort -integer $values]
return [list [lindex $s 0] [lindex $s end]]
}
lassign [minmax {5 2 9}] lo hi
puts "returned : $lo $hi"
# fills in the caller's variables
proc minmax_out {values loName hiName} {
upvar 1 $loName lo
upvar 1 $hiName hi
set s [lsort -integer $values]
set lo [lindex $s 0]
set hi [lindex $s end]
}
minmax_out {5 2 9} a b
puts "out-param: $a $b"returned : 2 9
out-param: 2 9Same answers, different contracts:
| Returning a value | upvar out-parameter | |
|---|---|---|
| Caller passes | values | names |
| Usable inside an expression | yes | no |
| Can be called on a computed value | yes | no — needs a variable first |
| Caller's variables touched | none | whichever names it passed |
| Reads as | a function | a command with effects |
The out-parameter version cannot be written as puts [minmax_out {5 2 9} a b], because its result is not the answer.
7. A worked example
A small set of list helpers that modify in place, in the style of lappend:
proc list_push {listVar value} {
upvar 1 $listVar l
lappend l $value
}
proc list_pop {listVar} {
upvar 1 $listVar l
if {[llength $l] == 0} { error "cannot pop from an empty list" }
set top [lindex $l end]
set l [lrange $l 0 end-1]
return $top
}
proc list_clear {listVar} {
upvar 1 $listVar l
set l {}
}
set stack {}
list_push stack a
list_push stack b
list_push stack c
puts "stack : $stack"
puts "popped : [list_pop stack]"
puts "stack : $stack"
list_clear stack
puts "cleared: '[set stack]' ([llength $stack] items)"
if {[catch {list_pop stack} m]} { puts "error : $m" }stack : a b c
popped : c
stack : a b
cleared: '' (0 items)
error : cannot pop from an empty listEach procedure takes a name, aliases it once at the top, and then works with an ordinary local. list_pop both modifies in place and returns a value, which is a reasonable combination when the returned thing is the item removed rather than the collection.
8. Exercises
Exercise 1. Why does push results "ok" pass results rather than $results?
Exercise 2. What do levels 1, #0 and 0 mean, and which is the default?
Exercise 3. What error does this give, and why?
proc f {v} {
upvar 1 v v
return $v
}
f helloExercise 4. Write bump_by taking the name of a counter and an amount, adding the amount to the caller's variable.
Exercise 5. Why is upvar the only way to pass an array to a procedure?
Exercise 6. Give two reasons to prefer returning a value over an upvar out-parameter, and the cases where upvar wins anyway.
Answers
1. Because upvar links to a variable by name. $results would substitute the list's contents, and the procedure would receive a value with no way to find the variable it came from.
2. 1 is the caller's frame, #0 is the global frame, and 0 is the procedure's own frame. 1 is the default when the level is omitted.
3. variable "v" already exists. The parameter v is a local created before the body runs, and upvar will not rebind a name that is already in use. Rename either the parameter or the alias.
4.
proc bump_by {countVar amount} {
upvar 1 $countVar c
if {![info exists c]} { set c 0 }
incr c $amount
}
set hits 5
bump_by hits 3
bump_by hits 2
puts $hits105. Because an array is not a value — there is nothing to pass or return, as Chapter 7.1 showed. Only its name can be handed over, and upvar is what turns that name back into usable access.
6. Returning a value can be used inside an expression and called on a computed argument, and it touches none of the caller's variables. upvar wins when the data is an array, when something must genuinely be modified in place, or when the surrounding code already works that way.
9. Remember
upvar ?level? otherName localNamemakes a local name refer to a variable in another frame.- The caller passes a bare name, not
$name— the procedure needs the name, not the value. - Level
1is the caller and is the default;#0is global;0is the procedure's own frame. Write the1anyway. - It creates the caller's variable if it does not exist, and
info existsthrough the link is a safe test beforehand. - It refuses a local name already in use —
variable "x" already exists— including when that name is a parameter. - Suffix the name-carrying parameter (
listVar,arrName) so the alias cannot collide. - Arrays can only be passed this way, which is the promise from Module 7 delivered.
- It is a weaker contract than returning a value — not usable in an expression, not callable on a computed argument. Use it for arrays, genuine in-place modification, or an existing convention.
Where this fits
Part of the Tcl curriculum.
