Version 9 of dict with

Updated 2014-02-19 10:01:28 by PeterLewerin

One of the subcommands of dict. Moves entries from a dictionary into variables, evaluates a script, and moves the (possibly updated) values back from the variables to the dictionary.

   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"
   }

The idea comes in part from the with statement of Pascal, which temporarily places the fields of a record into the local scope. A difference is that in Pascal the record fields merely shadow similarly-named variables, whereas dict with really puts the values into ordinary variables. Because of this, one should 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 safe for dictionaries which may gain new elements.

jima (2008-09-02)

Just a question...perhaps a silly one...

dict with creates new variables (taken 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. Should the variable i be created outside the for initialization 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 goes great with [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]]].


AMG: Be very careful to never access the with'ed dictionary directly inside the script body of the [dict with] command. 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 suggest optimizing [dict with] to not attempt to update the dictionary when its script argument is empty.

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"

  Elaboration on the "known set of entries" issue

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

The dict with command only sets the variable 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 {
    catch { unset 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