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 $arrayThe 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 arraySo, 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