Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 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"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
lo=1 hi=9

It 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc minmax {values} {
    set sorted [lsort -integer $values]
    return [list [lindex $sorted 0] [lindex $sorted end]]
}

puts [minmax {5 2 9 1}]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1 9

The caller unpacks with lassign from Chapter 6.14:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
lassign [minmax {5 2 9 1}] lo hi
puts "lo=$lo hi=$hi"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
lo=1 hi=9

One 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
count 4 total 17 min 1 max 9
total is 17
range is 1 to 9

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
lo=1 hi=4   <- hi is wrong

hi 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
old caller still works: total is 17
new field available   : mean is 4.25

5. 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.

SituationReturn
min and maxlist
a parsed key and valuelist
four statisticsdictionary
a record with named fieldsdictionary
anything a future version might add todictionary

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
2 ports
  clk is 1 bits
  data is 32 bits

This 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a=1 b=9

It 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
count    : 5
total    : 7
ok       : 0
failures : din, en
clean ok : 1

Note 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
first=p last=s

3. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
n=3 total=6

5. 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 lassign for two or three results.
  • Build it with list, so a result containing spaces stays one element.
  • Return a dictionary and read with dict get once 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 total versus lindex $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.