Tcl Part I · Module 7 · Dictionaries and Arrays
Arrays vs Dictionaries: Choosing One
Chapter 7.1 gave you a rule before you had seen either structure in anger. Now you have seen both completely, and the rule deserves a proper defence — including the parts where it is less clear-cut than it first looked. This chapter lays out the four axes that genuinely decide the question, measures the one everybody argues about, and is honest that the measurement does not matter nearly as much as people expect. It also deals with the situation most likely to come up in real work, which is that something else has already chosen for you.
Intermediate12 min readTclPart IDictionariesArraysDesign decisions
Part I · Module 7 · Chapter 7.11 · Arrays vs Dictionaries: Choosing One
1. The rule, restated
Use a dictionary when the data is something you pass, return, or store inside another structure. Use an array when you want variable-style access to named values inside a single scope — or when existing code hands you one.
Chapter 7.1 stated that before you had the vocabulary to evaluate it. Four things decide it, and they are not equally important.
2. Axis one: crossing a procedure boundary
This is the decisive one. A dictionary is a value, so it moves like any value:
proc summarise {d} {
return "[dict get $d mode] at width [dict get $d width]"
}
set cfg [dict create mode fast width 8]
puts [summarise $cfg]fast at width 8An array cannot do that. The equivalent needs the name and upvar:
proc summarise_arr {name} {
upvar 1 $name a
return "$a(mode) at width $a(width)"
}
array set opt {mode fast width 8}
puts [summarise_arr opt]fast at width 8Both work. They are not equivalent in what they commit you to:
| Dictionary | Array | |
|---|---|---|
| What the caller passes | a value | a variable name |
| What the procedure can do | read its own copy | reach into the caller's scope |
| If the procedure modifies it | caller unaffected | caller's data changes |
| Can the caller pass a literal? | yes | no |
| Can it be returned? | yes | only flattened |
The upvar form is not merely more typing — it is a different contract. The procedure now writes into its caller, which is harder to reason about and impossible to call with a value you just computed:
puts [summarise [dict create mode fast width 8]]fast at width 8There is no array equivalent of that line. You would have to assign to a temporary variable first.
3. Axis two: nesting
A dictionary goes inside a list, inside another dictionary, inside an array element:
set rows [list [dict create n u1 w 8] [dict create n u2 w 16]]
puts "[llength $rows] records, second is [dict get [lindex $rows 1] n]"
set store(cfg) [dict create width 8]
puts "inside an array element: [dict get $store(cfg) width]"2 records, second is u2
inside an array element: 8An array goes nowhere. If your data is a collection of records, the records must be dictionaries — there is no choice to make.
4. Axis three: order
Dictionaries preserve insertion order. Arrays have no order at all.
set d [dict create]
foreach x {alpha beta gamma delta} { dict set d $x 1 }
puts "dict : [dict keys $d]"
foreach x {alpha beta gamma delta} { set A($x) 1 }
puts "array : [array names A]"dict : alpha beta gamma delta
array : beta delta alpha gammaIf the order of your data carries meaning — the stages of a flow, the columns of a report, the sequence a user listed — a dictionary records it for free and an array loses it permanently. That is not something you can add back later: by the time you call array names, the information is gone.
5. Axis four: what the syntax makes easy
This is where arrays genuinely win. Counting:
foreach r {pass fail pass pass} { incr tally($r) }
puts [lsort [array names tally]]
puts "pass = $tally(pass)"fail pass
pass = 3versus:
set t [dict create]
foreach r {pass fail pass pass} { dict incr t $r }
puts "pass = [dict get $t pass]"pass = 3dict incr exists and is fine. But $tally(pass) reads like a variable because it is one, and in a block doing a lot of this, the array version has less punctuation per line. The same goes for append, and for info exists.
6. The axis everybody asks about: speed
Measured on this machine with time, 20,000 iterations over a 200-element structure:
set N 20000
array set A {}
set D [dict create]
for {set i 0} {$i < 200} {incr i} { set A(k$i) $i ; dict set D k$i $i }
puts "read array : [time {set v $A(k100)} $N]"
puts "read dict : [time {set v [dict get $D k100]} $N]"
puts "write array : [time {set A(k100) 7} $N]"
puts "write dict : [time {dict set D k100 7} $N]"read array : 0.09926665 microseconds per iteration
read dict : 0.105375 microseconds per iteration
write array : 0.0770708 microseconds per iteration
write dict : 0.3131979 microseconds per iterationReads are a tie — about six per cent apart, which is noise. Writes are not: the array is roughly four times faster, and that ratio reproduced across repeated runs.
Now the honest reading of those numbers. The difference on a write is about a quarter of a microsecond. To lose a tenth of a second to it you need around four hundred thousand writes. Below that, this measurement should not influence your choice at all — and if you are above it, you have a loop worth looking at for other reasons too.
7. When something else has already chosen
Often the decision is not yours.
Tcl hands you arrays. env and tcl_platform are arrays (Chapter 7.8 §7). You read them as arrays:
puts "platform : $tcl_platform(platform)"
puts "PATH set : [info exists env(PATH)]"platform : unix
PATH set : 1Existing code sets the convention. If a project's procedures all take an array name and upvar it, a single procedure that takes a dictionary is a seam everyone has to think about. Match the surrounding code; consistency is worth more than a marginally better structure.
A library's interface decides. If something hands you array get output, you have a dictionary-shaped list whether you wanted one or not — and that is exactly Chapter 7.12.
8. The decision, as a checklist
Work down the list and stop at the first match:
- Does it cross a procedure boundary as data? → dictionary.
- Does it go inside a list, a record, or another structure? → dictionary.
- Does its order mean anything? → dictionary.
- Did Tcl or existing code hand it to you as an array? → array.
- Is it scope-local tallying with names computed at run time? → array is comfortable; a dictionary is fine too.
- Anything else → dictionary.
Four of the six point one way, which is why "default to a dictionary" is reasonable shorthand — as long as you know what the exceptions are, rather than treating arrays as obsolete.
9. A worked comparison
The same job, both ways, so you can see the shape of each:
# Dictionary — the record is a value and comes back out
proc tally_dict {results} {
set t [dict create]
foreach r $results { dict incr t $r }
return $t
}
set counts [tally_dict {pass fail pass pass skip}]
puts "returned: $counts"
puts "pass: [dict get $counts pass]"returned: pass 3 fail 1 skip 1
pass: 3# Array — the procedure fills in the caller's variable
proc tally_array {results name} {
upvar 1 $name t
foreach r $results { incr t($r) }
}
tally_array {pass fail pass pass skip} counts2
puts "pass: $counts2(pass)"
puts "all : [lsort [array names counts2]]"pass: 3
all : fail pass skipThe dictionary version is a function: give it input, get a value. The array version is a procedure with an out-parameter. Both are legitimate Tcl; the first composes, and the second does not — you cannot write puts [tally_array ...].
Note too that the dictionary came back in the order the results first appeared, and the array needed lsort to be presentable at all.
10. Exercises
Exercise 1. For each, name the structure and the axis that decides it: (a) a per-file line count built in one procedure and printed sorted; (b) a parsed command line returned to the caller; (c) the stages of a flow, in order, each with settings; (d) env.
Exercise 2. Why can you write summarise [dict create a 1] but not the array equivalent?
Exercise 3. A procedure takes an array name, upvars it, and modifies it. Rewrite it as a dictionary version and state what changes for the caller.
Exercise 4. Measured here, a dictionary write took about 0.31 µs and an array write about 0.077 µs. How many writes before the difference reaches 0.1 s, and what does that imply?
Exercise 5. Someone argues "arrays are faster, so use arrays". Give two structural costs that outweigh the measurement.
Exercise 6. Write the same "count the words" job twice — once returning a dictionary, once filling a caller's array — and say which you would put in a shared library, and why.
Answers
1. (a) array — scope-local tallying, and it never leaves the procedure; (b) dictionary — it is returned, so it must be a value; (c) dictionary — the order carries meaning and the settings nest; (d) array — Tcl chose for you.
2. Because an array is not a value, so there is nothing to pass. The array form requires a name, and [dict create a 1] produces a value with no name attached. You would have to assign it to a variable first.
3.
# before
proc add_default_arr {name key value} {
upvar 1 $name a
if {![info exists a($key)]} { set a($key) $value }
}
# after
proc add_default_dict {d key value} {
if {![dict exists $d $key]} { dict set d $key $value }
return $d
}
set cfg [dict create mode fast]
set cfg [add_default_dict $cfg retries 1]
puts $cfgmode fast retries 1The caller must now assign the result. In exchange, the procedure cannot modify anything it was not given, and it can be called on a value.
4. The difference is about 0.31 − 0.077 ≈ 0.23 µs per write, so 0.1 s needs roughly 0.1 / 0.00000023 ≈ 430,000 writes. It implies the measurement is irrelevant to almost every script, and that the choice should be made on structure.
5. Any two of: a dictionary can be returned from a procedure and an array cannot; a dictionary can be stored in a list or another record and an array cannot; a dictionary preserves insertion order and an array destroys it; a dictionary can be passed as a computed value with no intermediate variable.
6.
proc count_dict {words} {
set t [dict create]
foreach w $words { dict incr t $w }
return $t
}
proc count_arr {words name} {
upvar 1 $name t
foreach w $words { incr t($w) }
}
puts [count_dict {clk rst clk}]
count_arr {clk rst clk} c
puts "[lsort [array names c]] -> $c(clk)"clk 2 rst 1
clk rst -> 2The dictionary version belongs in a shared library: it is a function, callers can compose it, it does not reach into anyone's scope, and it does not require its callers to agree on an out-parameter convention.
11. Remember
- Four axes decide it: passing, nesting, order, and syntax comfort. Speed is a distant fifth.
- A dictionary crosses procedure boundaries as data; an array requires a name and
upvar, which is a different contract, not just more typing. - Only dictionaries nest. A collection of records means records that are dictionaries.
- Dictionaries preserve insertion order; arrays destroy it irrecoverably.
- Arrays make scope-local counting read better —
incr tally($r)is a variable, not a lookup. That is the real argument for them. - Measured here: reads tie; array writes are about four times faster. The gap is around a quarter of a microsecond, so it needs hundreds of thousands of writes to matter.
- Do not choose for speed. The structural costs of the wrong choice are far larger.
env,tcl_platformand existing project conventions decide for you. Match the code around you.
Where this fits
Part of the Tcl curriculum.
