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
upvarcommand 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 setcommand 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
foreachto 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 withcommand. 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