Tcl Part I · Module 10 · Procedures and Reuse
Returning Lists and Dictionaries
A procedure can only return one value, and quite often you want it to produce several — a count and a total, a minimum and a maximum, a parsed record with four fields. The wrong answer, and the one people reach for first, is to write the extras into globals and let the caller pick them up. The right answer is that one value in Tcl can hold as many results as you like, because lists and dictionaries are values. This chapter shows both shapes, how callers unpack them, and the rule for choosing between them — which comes down to what happens to existing callers when you add a fifth result next year.
Foundation11 min readTclPart IProceduresReturn valuesDictionaries
Part I · Module 10 · Chapter 10.8 · Returning Lists and Dictionaries
1. The problem
A procedure that computes two things has to get both back to its caller:
# the tempting wrong answer
set g_lo 0
set g_hi 0
proc minmax_global {values} {
global g_lo g_hi
set sorted [lsort -integer $values]
set g_lo [lindex $sorted 0]
set g_hi [lindex $sorted end]
}
minmax_global {5 2 9 1}
puts "lo=$g_lo hi=$g_hi"lo=1 hi=9It works, and it has every problem Chapter 10.6 §7 described: the procedure's effect is invisible at the call site, two calls cannot be in flight at once, and you cannot use the result in an expression without a statement first.
2. Return a list
proc minmax {values} {
set sorted [lsort -integer $values]
return [list [lindex $sorted 0] [lindex $sorted end]]
}
puts [minmax {5 2 9 1}]1 9The caller unpacks with lassign from Chapter 6.14:
lassign [minmax {5 2 9 1}] lo hi
puts "lo=$lo hi=$hi"lo=1 hi=9One value out, two names in, nothing global, and the call can appear anywhere a value can.
3. Return a dictionary
Once there are more than two or three results, names beat positions:
proc stats {values} {
set total 0
foreach v $values { incr total $v }
set sorted [lsort -integer $values]
return [dict create \
count [llength $values] \
total $total \
min [lindex $sorted 0] \
max [lindex $sorted end]]
}
set s [stats {5 2 9 1}]
puts $s
puts "total is [dict get $s total]"
puts "range is [dict get $s min] to [dict get $s max]"count 4 total 17 min 1 max 9
total is 17
range is 1 to 9The caller takes what it needs by name and ignores the rest.
4. Why that matters: adding a result later
Here is the difference that decides the choice. Suppose the procedure grows a fifth result.
With a list, every existing caller's lassign is now wrong:
proc minmax2 {values} {
set sorted [lsort -integer $values]
# a new result inserted in the middle
return [list [lindex $sorted 0] [llength $values] [lindex $sorted end]]
}
lassign [minmax2 {5 2 9 1}] lo hi
puts "lo=$lo hi=$hi <- hi is wrong"lo=1 hi=4 <- hi is wronghi is 4, which is the count. No error — the old caller silently reads the wrong field. This is the maintainability argument Chapter 6.14 made about positional records, now at a procedure boundary.
With a dictionary, the old caller is unaffected:
proc stats2 {values} {
set total 0
foreach v $values { incr total $v }
set sorted [lsort -integer $values]
return [dict create \
count [llength $values] total $total \
min [lindex $sorted 0] max [lindex $sorted end] \
mean [expr {$total / double([llength $values])}]]
}
set s [stats2 {5 2 9 1}]
puts "old caller still works: total is [dict get $s total]"
puts "new field available : mean is [dict get $s mean]"old caller still works: total is 17
new field available : mean is 4.255. The rule
Use a list for two or three results whose order is obvious and will not change. Use a dictionary as soon as the results have names worth having, or the set might grow.
| Situation | Return |
|---|---|
| min and max | list |
| a parsed key and value | list |
| four statistics | dictionary |
| a record with named fields | dictionary |
| anything a future version might add to | dictionary |
In practice that means: reach for a list when you would have used a tuple, and a dictionary the moment you find yourself writing a comment to explain what position 3 is.
6. Nesting works
Because both are values, a procedure can return a list of dictionaries, or a dictionary containing lists:
proc parse_ports {specs} {
set out {}
foreach spec $specs {
lassign $spec name dir width
lappend out [dict create name $name dir $dir width $width]
}
return $out
}
set ports [parse_ports {{clk in 1} {data out 32}}]
puts "[llength $ports] ports"
foreach p $ports {
puts " [dict get $p name] is [dict get $p width] bits"
}2 ports
clk is 1 bits
data is 32 bitsThis is the shape Chapter 7.1 §8 introduced, now produced by a procedure rather than written by hand.
7. The out-parameter alternative, and why not
Tcl can also fill in a caller's variable, using upvar:
proc minmax_out {values loName hiName} {
upvar 1 $loName lo
upvar 1 $hiName hi
set sorted [lsort -integer $values]
set lo [lindex $sorted 0]
set hi [lindex $sorted end]
}
minmax_out {5 2 9 1} a b
puts "a=$a b=$b"a=1 b=9It works. It is also a different contract: the caller passes names, the procedure reaches into their frame, and the call cannot be used as a value — puts [minmax_out ...] would print nothing useful.
8. A worked example
proc analyse {rows} {
if {[llength $rows] % 2 != 0} {
error "rows must be name-value pairs"
}
set count 0
set total 0
set failures {}
foreach {name value} $rows {
incr count
incr total $value
if {$value < 0} { lappend failures $name }
}
return [dict create \
count $count \
total $total \
failures $failures \
ok [expr {[llength $failures] == 0}]]
}
set r [analyse {clk 3 rst 0 din -2 dout 7 en -1}]
puts "count : [dict get $r count]"
puts "total : [dict get $r total]"
puts "ok : [dict get $r ok]"
puts "failures : [join [dict get $r failures] {, }]"
set clean [analyse {a 1 b 2}]
puts "clean ok : [dict get $clean ok]"count : 5
total : 7
ok : 0
failures : din, en
clean ok : 1Note failures is itself a list inside the dictionary, and it survived with its elements intact because lappend built it properly.
9. Exercises
Exercise 1. Why can proc f {} { return "a b" } not be distinguished by the caller from a procedure returning two separate results?
Exercise 2. Write first_last returning a list of the first and last elements, and show a caller using lassign.
Exercise 3. What goes wrong when a procedure that returned 2 elements starts returning 3, with callers using lassign?
Exercise 4. Rewrite this to return a dictionary instead of using globals:
proc count_global {l} {
global g_total g_n
set g_n [llength $l]
set g_total 0
foreach v $l { incr g_total $v }
}Exercise 5. Give the rule for choosing between returning a list and returning a dictionary.
Exercise 6. When is an out-parameter with upvar genuinely the right choice?
Answers
1. It cannot be distinguished, because "a b" is a two-element list — that is what Chapter 6.1 established. Whether it represents one string containing a space or two results depends entirely on how the caller reads it, which is why building with list matters: [list "a b"] is one element and "a b" is two.
2.
proc first_last {l} {
return [list [lindex $l 0] [lindex $l end]]
}
lassign [first_last {p q r s}] f l
puts "first=$f last=$l"first=p last=s3. If the new result is appended at the end, existing lassign calls still bind the first two correctly and simply ignore the extra. If it is inserted anywhere earlier, every caller silently reads shifted fields — no error, just wrong values. That is the risk a dictionary removes.
4.
proc count_pairs {l} {
set total 0
foreach v $l { incr total $v }
return [dict create n [llength $l] total $total]
}
set r [count_pairs {1 2 3}]
puts "n=[dict get $r n] total=[dict get $r total]"n=3 total=65. A list for two or three results with an obvious, stable order; a dictionary as soon as the results deserve names or the set might grow. Adding a field to a list can silently shift every caller; adding a key to a dictionary cannot.
6. When the data is an array, which is not a value and therefore cannot be returned at all, or when the surrounding code already uses that convention and consistency is worth more than the better interface.
10. Remember
- A procedure returns one value, and one value can hold many results — lists and dictionaries are values.
- Return a list and unpack with
lassignfor two or three results. - Build it with
list, so a result containing spaces stays one element. - Return a dictionary and read with
dict getonce the results have names or might grow. - Adding a field to a returned list can silently shift every caller's
lassign; adding a key to a returned dictionary breaks nothing. - A dictionary documents itself at the call site —
dict get $s totalversuslindex $r 2. - Nesting works: a list of dictionaries, a dictionary containing lists.
- Prefer returning a value to filling a caller's variable. Out-parameters are for arrays, which cannot be returned, and for matching existing conventions.
- Never use globals to carry extra results out.
Where this fits
Part of the Tcl curriculum.
