Dictionaries as alternative to arrays

Dictionaries as alternative to arrays

Arrays have a number of drawbacks:
  • They are actually collections of variables and as such can not contain other arrays.
  • Passing them to a procedure means that you have to pass the name and use the upvar command to actually use them inside the procedure.
  • Multidimensional arrays (that is, arrays whose index consists of two or more parts) have to be emulated with constructions like:
    set array(1,2) 10
    set array(2,2) 11
    
    This is quite possible, but it can become very clumsy (there can be no intervening spaces for instance).

In Tcl 8.5 the dict command has been introduced. This provides efficient access to key-value pairs, just like arrays, but dictionaries are pure values. This means that you can pass them to a procedure just as a list or a string, without the need for dict.

Unlike arrays, you can nest dictionaries, so that the value for a particular key consists of another dictionary. That way you can elegantly build complicated data structures, such as hierarchical databases.

Here is an example (adapted from the man page):

#
# Create a dictionary:
# Two clients, known by their client number,
# with forenames, surname
#
dict set clients 1 forenames Joe
dict set clients 1 surname   Schmoe
dict set clients 2 forenames Anne
dict set clients 2 surname   Other

#
# Print a table
#
puts "Number of clients: [dict size $clients]"
dict for {id info} $clients {
    puts "Client $id:"
    dict with info {
       puts "   Name: $forenames $surname"
    }
}

What happens in this example is:

  • We fill a dictionary, called clients, with the information we have on two clients. The dictionary has two keys, "1" and "2" and the value for these keys is an dictionary - again with two keys "forenames" and "surname". This is because the dict set command accepts a list of keywords (descending into the nesting of the dictionaries) and uses the last argument as the actual value.
  • Then we use a kind of foreach to loop over the contents of the dictionary (only the first level!).
  • To get at the actual values in the dictionary that is stored with the client IDs we use the dict with command. This command takes the dictionary and sets variables by the name of the keys to the values in that dictionary. That way the contents is readily available via these variables.

Done up to this point

# # Get names and values directly # foreach {name value} [array get mydata] { puts "Data on \"$name\": $value" } Note, however, that the elements will not be returned in any predictable order: this has to do with the underlying "hash table". If you want a particular ordering (alphabetical for instance), use code like:
foreach name [lsort [array names mydata]] {
    puts "Data on \"$name\": $mydata($name)"
}
While arrays are great as a storage facility for some purposes, they are a bit tricky when you pass them to a procedure: they are actually collections of variables. This will not work:
proc print12 {a} {
   puts "$a(1), $a(2)"
}

set array(1) "A"
set array(2) "B"

print12 $array
The reason is very simple: an array does not have a value. Instead the above code should be:
proc print12 {array} {
   upvar $array a
   puts "$a(1), $a(2)"
}

set array(1) "A"
set array(2) "B"

print12 array
So, instead of passing a "value" for the array, you pass the name. This gets aliased (via the upvar command) to a local variable (that behaves the as original array). You can make changes to the original array in this way too.

Example

#
# The example of the previous lesson revisited - to get a
# more general "database"
#

proc addname {db first last} {
    upvar $db name

    # Create a new ID (stored in the name array too for easy access)

    incr name(ID)
    set id $name(ID)

    set name($id,first) $first   ;# The index is simply a string!
    set name($id,last)  $last    ;# So we can use both fixed and
                                 ;# varying parts
}

proc report {db} {
    upvar $db name

    # Loop over the last names: make a map from last name to ID

    foreach n [array names name "*,last"] {
        #
        # Split the name to get the ID - the first part of the name!
        #
        regexp {^[^,]+} $n id

        #
        # Store in a temporary array:
        # an "inverse" map of last name to ID)
        #
        set last       $name($n)
        set tmp($last) $id
    }

    #
    # Now we can easily print the names in the order we want!
    #
    foreach last [lsort [array names tmp]] {
        set id $tmp($last)
        puts "   $name($id,first) $name($id,last)"
    }
}

#
# Initialise the array and add a few names
#
set fictional_name(ID) 0
set historical_name(ID) 0

addname fictional_name Mary Poppins
addname fictional_name Uriah Heep
addname fictional_name Frodo Baggins

addname historical_name Rene Descartes
addname historical_name Richard Lionheart
addname historical_name Leonardo "da Vinci"
addname historical_name Charles Baudelaire
addname historical_name Julius Caesar

#
# Some simple reporting
#
puts "Fictional characters:"
report fictional_name
puts "Historical characters:"
report historical_name