Tcl Part I · Module 7 · Dictionaries and Arrays
dict with and dict update
These two commands let you stop writing dict get and dict set for a while and work on a dictionary's contents as ordinary variables. That is genuinely pleasant for a block of arithmetic, and one of the two does something you need to know about before you use it: it creates a local variable for every key, overwriting anything you already had by that name, and it does not put it back afterwards. This chapter shows both commands completely, including the write-back rules, which are not what most people assume — and then gives a straight recommendation about which to reach for.
Intermediate13 min readTclPart IDictionariesdict withdict update
Part I · Module 7 · Chapter 7.7 · dict with and dict update
1. The idea
When a block of code reads and writes several keys of the same dictionary, dict get and dict set get noisy:
set cfg [dict create width 8 depth 16]
dict set cfg total [expr {[dict get $cfg width] * [dict get $cfg depth]}]
puts [dict get $cfg total]128dict with lets the same block use plain variables:
set cfg [dict create width 8 depth 16]
dict with cfg {
set total [expr {$width * $depth}]
}
puts $cfgwidth 8 depth 16Look closely at that output — total is not there. That is the first rule, and section 5 explains it. Set an existing key instead and the change does land:
set cfg [dict create width 8 depth 16 total 0]
dict with cfg {
set total [expr {$width * $depth}]
}
puts $cfgwidth 8 depth 16 total 1282. The shape of both commands
dict with dictVariable ?key ...? {body}
dict update dictVariable key varName ?key varName ...? {body}dict with makes a variable for every key. dict update makes variables only for the keys you name, and lets you choose the variable names:
set cfg [dict create mode fast runs 3 width 8]
dict update cfg runs r width w {
incr r 10
set w 64
}
puts $cfgmode fast runs 13 width 64Both take the dictionary variable name, bare — they modify it. That is the rule from Chapter 7.2 §7.
3. The trap: dict with overwrites your locals
This is the one to remember from the whole chapter.
proc demo {} {
set runs 999
set mode "careful"
set c [dict create runs 3]
puts "before: runs=$runs mode=$mode"
dict with c {
puts "inside: runs=$runs mode=$mode"
}
puts "after : runs=$runs mode=$mode"
}
demobefore: runs=999 mode=careful
inside: runs=3 mode=careful
after : runs=3 mode=carefulRead the first and last lines. runs was 999 before the block and is 3 after it. The dictionary had a key called runs, so dict with created a local variable of that name — destroying the one that was already there — and never restored it. mode survived only because the dictionary had no key by that name.
4. The variables outlive the block
Related, and almost as surprising: the variables dict with creates are ordinary locals. They do not disappear at the closing brace.
set c [dict create mode fast]
dict with c { }
puts "mode after the block: $mode"mode after the block: fastdict with is not a scope. It is a command that sets some variables, runs your body, and reads them back.
5. What gets written back
Three rules, and they are not symmetric.
An existing key whose variable changed is written back:
set d [dict create a 1]
dict with d { set a 42 }
puts $da 42A brand-new variable does not become a key:
set d [dict create a 1]
dict with d { set brandnew 99 }
puts "$d | key added? [dict exists $d brandnew]"a 1 | key added? 0Unsetting a variable does remove its key:
set d [dict create a 1 b 2]
dict with d { unset b }
puts "$d | b still there? [dict exists $d b]"a 1 | b still there? 0So the block can change values and delete keys, and cannot add them. Section 1's missing total was this rule in action.
6. Both take a name
set c [dict create mode fast runs 3]
dict with $c { puts $mode }can't read "mode fast runs 3": no such variable$c substituted to the dictionary's text, and dict with then looked for a variable with that name.
Compare this with the same mistake on dict set, from Chapter 7.2 §7: there, Tcl happily created a variable with that name and the error was silent. Here it is trying to read one first, so you get told. The underlying mistake is identical; only the visibility differs — which is a good reason not to rely on being told.
7. The body failing does not prevent the write-back
Worth knowing before you put anything risky in the body:
set g [dict create a 1]
catch {
dict with g {
set a 99
error "something went wrong"
}
}
puts $ga 99The body errored, and a is still 99. dict update behaves the same way:
set h [dict create a 1]
catch {
dict update h a x {
set x 99
error "something went wrong"
}
}
puts $ha 99This is deliberate — it is what keeps the dictionary consistent with the variables when a body exits early, including via return or break. It is not a transaction. If you need all-or-nothing, work on a copy and assign it at the end:
set safe [dict create a 1]
set attempt $safe
if {![catch {dict set attempt a 99 ; error "fail"}]} {
set safe $attempt
}
puts $safea 18. dict update is the one to prefer
dict update avoids every problem above except the write-back-on-error rule:
- It touches only the keys you name, so it cannot clobber a local you forgot about.
- You choose the variable names, so you can avoid a collision deliberately.
- It can create keys.
- Reading the first line tells you exactly which keys the block is about.
set cfg [dict create mode fast runs 3 width 8]
dict update cfg runs cfg_runs width cfg_width {
set cfg_runs [expr {$cfg_runs * 2}]
set cfg_width [expr {$cfg_width + 8}]
}
puts $cfgmode fast runs 6 width 169. A worked example
A statistics accumulator, written three ways, all producing the same result:
# 1 — plain dict get / dict set
proc record_plain {statsName ok} {
upvar 1 $statsName s
dict set s total [expr {[dict get $s total] + 1}]
if {$ok} {
dict set s pass [expr {[dict get $s pass] + 1}]
}
}
# 2 — dict update, naming exactly what it touches
proc record_update {statsName ok} {
upvar 1 $statsName s
dict update s total t pass p {
incr t
if {$ok} { incr p }
}
}
set stats [dict create total 0 pass 0]
foreach result {1 1 0 1 0} {
record_update stats $result
}
puts $stats
puts "pass rate: [format %.1f [expr {100.0 * [dict get $stats pass] / [dict get $stats total]}]]%"total 5 pass 3
pass rate: 60.0%Version 2 is shorter than version 1 and names its two keys on one visible line. A dict with version would be shorter still, and would also create locals called total and pass in a procedure that might well have had its own.
10. Exercises
Exercise 1. Predict all three lines:
proc f {} {
set width 100
set d [dict create width 8]
puts $width
dict with d { puts $width }
puts $width
}
fExercise 2. Why does this not add a total key, and what are two ways to make it?
set d [dict create a 2 b 3]
dict with d { set total [expr {$a * $b}] }Exercise 3. What does dict with $d {...} report, and how does that differ from dict set $d k v?
Exercise 4. Rewrite this dict with as a dict update that cannot clobber anything:
dict with cfg { set runs [expr {$runs * 2}] }Exercise 5. After this runs, what is $d?
set d [dict create a 1 b 2]
catch { dict with d { set a 50 ; unset b ; error "stop" } }Exercise 6. Write a procedure scale that multiplies every value in a dictionary by a factor and returns the new dictionary, without using dict with or dict update. Say why neither is a good fit here.
Answers
1.
100
8
8The dictionary's width key overwrote the local, and it was not restored.
2. Because dict with writes back only keys that already existed; a brand-new variable is discarded. Either seed the key first (dict create a 2 b 3 total 0), or use dict update d total t {set t ...}, which creates the key.
3. dict with $d gives can't read "...": no such variable — it tried to read a variable named after the dictionary's text. dict set $d k v creates such a variable instead and reports nothing at all, which is why it is the more dangerous of the two.
4.
dict update cfg runs cfg_runs {
set cfg_runs [expr {$cfg_runs * 2}]
}It names the single key it touches, and cfg_runs is unlikely to collide with anything the surrounding procedure owns.
5. a 50. The body errored, and both the assignment and the deletion were still written back — b is gone and a is 50.
6.
proc scale {d factor} {
set out [dict create]
dict for {k v} $d {
dict set out $k [expr {$v * $factor}]
}
return $out
}
puts [scale [dict create a 1 b 2 c 3] 10]a 10 b 20 c 30Neither fits: both operate on keys known in advance, and this procedure must work on whatever keys it is given. dict for is the right tool for "every pair".
11. Remember
dict with name {body}makes a local variable for every key;dict update name key var ... {body}makes them only for the keys you name.- Both take the dictionary's bare name — they modify it.
dict with $derrors;dict set $ddoes not. dict withoverwrites existing locals of the same name and does not restore them. This is its real cost.- The variables outlive the block. It is not a scope.
- Changing a value writes back; unsetting a variable deletes the key; a new variable is discarded.
dict updatecan create keys because you named them. - An error in the body does not prevent the write-back. Neither command is a transaction.
- Prefer plain
dict get/dict set; reach fordict updatewhen a block is genuinely noisy. Usedict withonly on dictionaries you built, in short blocks. - Read
dict withfluently anyway — existing Tcl is full of it.
Where this fits
Part of the Tcl curriculum.
