Version 24 of dict with

Updated 2018-09-28 13:01:49 by PeterLewerin

dict with, a subcommand of dict, puts dictionary values into variables named by the dictionary keys, 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]}

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

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.