'''[dict%|%dict with]''', a subcommand of `[dict]`, puts dictionary values into variables named by the dictionary keys, `[eval%|%evaluates]` the given script, and then reflects any changed variable values back into the dictionary. ** Synopsis ** : '''[dict with]''' ''dictionaryVariable'' ?''key ...''? ''body'' ** Description ** ====== set info [dict create forenames Joe surname Schmoe street {147 Short Street} \ city Springfield phone 555-1234] dict with info { puts " Name: $forenames $surname" puts " Address: $street, $city" puts " Telephone: $phone" } ====== `dict with` is inspired by the '''with''' statement of [Pascal], which temporarily places the fields of a record into the local scope. One difference is that in Pascal the record fields merely shadow similarly-named variables, whereas `dict with` creates/modifies variables in the current scope. Because of this, it's generally advisable to only use `dict with` to unpack dictionaries which have a known set of entries (like a Pascal record or [C] struct). An alternative technique is to unpack dictionaries into arrays using `[array set]` (and repack them with `[array get]`). This is safer since it avoids the unexpected creation or modification of variables in the current scope. ** Accessing the Dictionary From Within the Script ** [AMG]: Be very careful to never access the with'ed dictionary directly inside the script body of `dict with`. Neither reading nor writing are safe. Reading the dictionary will provide the value before the `dict with` script started, since it won't be updated from the unpacked variables until the script completes. Any writes to the dictionary will be lost when the script completes, since `dict with` overwrites the entire dictionary, even if none of the unpacked variables were changed. These limitations are particularly problematic when calling into other procedures that may want to access the dictionary, either as a `[global]` variable or via `[upvar]`. Earlier tonight I had a hell of a time tracking this problem down; I was invoking `[coroutine]` commands inside a `dict with` script, and all the coroutines were sharing the same dictionary. Within the coroutines everything seemed fine; things didn't go pear-shaped until after the outer `dict with` script finished, reverting all changes to the dictionary. Removing `dict with` fixed my problem. Another note. `dict with dictVar {}` overwrites $dictvar with itself. This wastes a little CPU time and may trigger an unexpected variable write `[trace]`, but it's otherwise harmless. I propose that `dict with` be optimized to not attempt to update the dictionary when its script argument is empty. : [DKF]: We currently write the results back because we're not sure about the presence (or otherwise) of traces and whether there's an entry in the dictionary called “`dictVar`”, but the code generated is a ''lot'' more optimal than when there's a real script (when lots of guard code to handle exceptions and things like that is required). : [AMG]: Understood, but this is useful functionality which I abuse frequently in [Wibble]. Perhaps create a new command that works like [[dict with]] but has no script argument and does not attempt to write back into the dict. Make it just take the dict value, rather than a variable name. Here's a script that doesn't work: ====== set d {a 1 b 2} ;# returns "a 1 b 2" dict with d { set d ;# returns "a 1 b 2" dict get $d a ;# returns "1" set a ;# returns "1" set d {a 3 b 4} ;# returns "a 3 b 4" set d ;# returns "a 3 b 4" dict get $d a ;# returns "3" set a ;# returns "1" } set d ;# returns "a 1 b 2" ====== ** Take Care When Using Dictionaries That May Have Unknown Keys ** The "one should only use dict with to unpack dictionaries which have a known set of entries" rule above is important. Of course, like all rules it can be broken when necessary, but be prepared to take care of unwanted lingering variables. Take the following script, which for each dictionary is supposed to print the values of `a`, `b`, and (if it exists in the dictionary) `c`: ====== set dicts [list [dict create a 1 b 2] [dict create a 3 b 4 c 5] [dict create a 6 b 7]] foreach dict $dicts { dict with dict { puts -nonewline "\$a=$a \$b=$b" if {[info exists c]} { puts -nonewline " \$c=$c" } puts {} } } # output (invalid!): $a=1 $b=2 $a=3 $b=4 $c=5 $a=6 $b=7 $c=5 ====== `dict with` only sets `$c` during the second iteration, so it doesn't exist during the first iteration. It ''does'' exist during the third iteration as a leftover from the second, though. If you don't want that, you need to `[unset]` it yourself between the invocations of `dict with`: ====== set dicts [list [dict create a 1 b 2] [dict create a 3 b 4 c 5] [dict create a 6 b 7]] foreach dict $dicts { unset -nocomplain c dict with dict { puts -nonewline "\$a=$a \$b=$b" if {[info exists c]} { puts -nonewline " \$c=$c" } puts {} } } # output (valid): $a=1 $b=2 $a=3 $b=4 $c=5 $a=6 $b=7 ====== Doing it ''after'' the `dict with` block would seem more logical but doesn't help you if `c` has already been set. To be really sure, you should nuke from orbit... I mean, `[unset]` each one of the variables that `dict with` is supposed to create before each time you invoke the command. (''And'' `unset` them again when you are done with the `dict with` work, to avoid polluting downstream code.) The problem can of course be sidestepped by calling a `[proc]` to do the work (in which case the variables are created in a fresh [stack frame] that is discarded when the `proc` returns): ====== set dicts [list [dict create a 1 b 2] [dict create a 3 b 4 c 5] [dict create a 6 b 7]] proc printTheMembers {dict} { dict with dict { puts -nonewline "\$a=$a \$b=$b" if {[info exists c]} { puts -nonewline " \$c=$c" } puts {} } } foreach dict $dicts { printTheMembers $dict } # output (valid): $a=1 $b=2 $a=3 $b=4 $c=5 $a=6 $b=7 ====== [AMG]: For even more fun, have keys containing apparent namespace and array components. As expected (though you probably didn't think about this before), the following code creates an array: ====== set myDict {array(1) one array(2) two} dict with myDict {} ====== And this code results in an error: ====== set myDict {array(1) one array two} dict with myDict {} ====== This sets a global variable: ====== proc sub {} { set myDict {::var val} dict with myDict {} } sub ====== ---- [samoc]: An alternative approach that I use to handle unknown-key safety is to emulate "lassign" and name the keys that I want to extract: ====== set d {a 1 b 2 c 3} assign $d a b assert {$a == 1} assert {$b == 2} assert {![info exists c]} ====== https://github.com/samoconnor/oclib.tcl/blob/master/oclib/oc_base-1.0.tm#L81%|%"assign" implementation here%|% ---- [gchung]: Another implementation of "assign" but it also returns a dict of entries not assigned: ====== package require Tcl 8.6 proc ::dict_assign {dict args} { # Assign values in "dict" with keys in "args" to variables of the same # name as the keys. Returns a dict with entries that were not assigned. uplevel 1 [list lassign [lmap key $args { dict get $dict $key }] {*}$args] return [dict remove $dict {*}$args] } set others [dict_assign {a 1 b 2 c 3 d 4} b d] # => returns {a 1 c 3} ====== **Destruction of Dictionary Key Variables** jima 2008-09-02: Just a question...perhaps a silly one... `dict with` creates new variables from the entries of the dictionary and evaluates a script that might use them. So far so good. Would it make sense to destroy the created variables immediately after the execution of the script? I think this way there would be no cluttering at the level that called '''dict with''' caused by the new variables. I mean: ====== set a [dict create foo bar] dict with a { puts $foo } ====== #Now I have from now on another foo variable that might be considered 'clutter' from this point on... In a sense, I feel the same about variables created in `for {set i 0} ...`, they remain after the loop has completed. If `$i` already existed prior to the initialization of `[for]`, then I think it should stay, but otherwise...would it not be cleaner to remove it ? [DKF]: It's never worked like that in the past. [AMG]: As a consequence, it's very easy to unpack a dict into local variables: ====== proc print_stuff {args} { dict with args {} puts "a=$a b=$b locals={[info locals]}" } print_stuff a 1 b 2 c 3 # prints "a=1 b=2 locals={args a b c}" ====== Of course, changes made to the local variables created in this way don't magically find their way back into the original dict. The "`dict with args {}`" idiom is a great companion to `[coroutine]`. `[foreach] [{*}]$var {}` works the same as `dict with var {}` when $var is a dict whose keys and values are all single-element lists. Also consider `[lassign] [[[dict values] $var]] {*}[[[dict keys] $var]]`. [samoc] 2014-05-16: Variable pollution safe dict_with using {*}: ====== dict with d { ... } unset {*}[dict keys $d] ====== [PYK] 2014-05-16: That's an incomplete "solution", since it runs the risk of unsetting vars that already existed in the scope. [Larry Smith] I had added a question about how to add a key and a variable inside a "with" block. See [My dict with] for my solution. <> Command