A lot of my Tcl code is basically state-enginesque; that is, if it were written in C, all the structs would be global (and very large). So I just use large global arrays for 90 percent of my data storage. I have thus been sheltered from the one significant limitation of arrays, (but you may have run into it): arrays are not first-class objects. You can't pass or return an array by value, only by name.
When you want to pass structured packages of data between procs by value, without reference to a lot of upvar levels and passing by name (or the messy expedient of making most of your variable space global, when your code is not a state engine), then you generally use a list:
proc foo mylist { global ofp lassign $mylist last first phone echo $ofp [format "%-15s %-15s %03d-%04d" Last First Phone] echo $ofp [format "%-15s %-15s %03d-%04d" $last $first $phone] ... } lappend person $last_name lappend person $first_name lappend person $phone_num set res [foo $person] ...The only clue to the meaning of the list elements is their order; that's not so painful, but this kind of code can present maintenance problems later, especially if different programmers work on different modules. If you choose good variable names, as above, it's almost self-documenting; but if you get sloppy and start referring to your list elements by raw index numbers
if {[lindex $mylist 0] == $test_name} { do_something }the code starts to lose legibility. Who can tell, without poring over (perhaps a lot of) other source, whether the name being compared is a first or a last name?
Keyed lists were introduced into TclX to provide a first-class object that could be passed and returned by value, yet had internal structure AND was self-documenting and conducive to good coding. You could say that keyed lists are a stylistic, rather than a purely functional, enhancement. They offer you a syntactic construct that encourages good programming style. Functionally they resemble (as we said) structs in C. A keyed list in its simple form is just a way to store keyword/value pairs:
tcl> keylset person LAST Flintstone FIRST Fred PHONE 333-4444 \ OCCUP Toon SALARY 0 NOTES "What a swell guy" tcl> echo $person {LAST Flintstone} {FIRST Fred} {PHONE 333-4444} {OCCUP Toon} {SALARY 0} {NOTES {What a swell guy}} tcl> keylget person OCCUP ToonSo there's the basic idea: you make a keyed-list by using the command keylset listVar keyString Value [keyString Value...], and you can then retrieve individual values out of it by keyword using keylget listVar keyString
As you can see, a keyed list is just a list of lists. There's no radical new Tcl variable type here, just some procedures that allow you to create and manipulate a list of lists easily and concisely. Naturally the story does not end here. You can also delete a keyword and its associated value out of the list:
tcl> keyldel person SALARY tcl> echo $person {LAST Flintstone} {FIRST Fred} {PHONE 333-4444} {OCCUP Toon} {NOTES {What a swell guy}}or add a new one
tcl> keylset person HOBBIES "Lithography, Zoetropes, Oenology" {LAST Flintstone} {FIRST Fred} {PHONE 333-4444} {OCCUP Toon} {NOTES {What a swell guy}} {HOBBIES {Lithography, Geosophy, Zoetropes}}And, as with the array names command, you can retrieve the list of valid keys set for this keyed list:
tcl >keylkeys person LAST FIRST PHONE OCCUP NOTES HOBBIESNow you have a data storage convention which has the benefits of an array (named storage locations whose names are retrievable from the storage object itself) and of a list (can be passed by value). The benefits of passing by value become more obvious when we go back to our client and server example. If I want to pass a structured package of information from the client to the server or vice versa, neither has any knowledge of the other's variable space; I have to pass by value. It would be nice if I could examine the argument I had just been passed to see what I was given, rather than relying on hard-coded index positions and arcane rules ("if the first word of the list is X, then position 3 is the phone number").
Let's say the client has the social security number of a person, and it wants to know what the server knows about that person. The client might contain some code like this:
server_send $fp "$ssn" select $fp {} {} set err [catch {gets $fp answer} res] if {$err} { echo "No more server!\n$res" exit 0 } # Server returns to me a keyed list which I call "answer" set flds [keylkeys answer] set patient "FIRST LAST FLOOR WARD BED CHARTNUM MEDS DIET PPHYS" set doctor "FIRST LAST DIVIS SPECIAL CASELOAD HOURS PAGER HOME" # If there are fields in the answer that are not patient fields # then it has to be a doctor; there are only two species in our taxonomy if {![lempty [lindex [lintersect3 $flds $patient] 0]]]} { set type "Doctor" } else { set type "Patient" } echo "$type [keylget answer FIRST] [keylget answer LAST]:" keyldel answer FIRST keyldel answer LAST foreach k [keylkeys answer] { echo "$k : [keylget answer $k]" }As with our previous client/server example, this is a bit silly; but it does demonstrate that I can pass a structured message by value and retrieve not only its contents but embedded information about the meaning of its contents.
As to maintainability, you can add fields to keyed lists without disturbing procs that already use those lists, because the procs can easily be written to ignore the length of the list and order of elements. There is no temptation to indulge in raw integer indexing, so code written with keyed lists will be as readable as the choice of keywords.
A keyed list can itself be a value element in a keyed list (structs can also contain structs in C). This makes them more powerful than mere keyword/value pairs.
tcl>keylset n LAST Smith FIRST Sybilla tcl>echo $n {LAST Smith} {FIRST Sybilla} tcl>keylset person NAME $n ADDR "1 Haresfoot Crescent" tcl>echo $person {NAME {{LAST Smith} {FIRST Sybilla}}} {ADDR {1 Haresfoot Crescent}} tcl> keylget person NAME.LAST Smith tcl>keylget person NAME {LAST Smith} {FIRST Sybilla}Here we made n a keyed list which became one element of the keyed list person. Note that we can now retrieve the subelements of NAME without resorting to [keylget [keylget...] ...] by means of the dot-syntax (NAME.LAST) -- this is in fact necessary, because the keyed list commands refer to their target lists by name and not value! We could also have set the nested list values using the same dot-syntax:
tcl>keylset person NAME.LAST Smith tcl>keylset person NAME.FIRST Shadrach tcl>echo $person {NAME {{LAST Smith} {FIRST Shadrach}}} {ADDR {1 Haresfoot Crescent}}Now for the catch: beware of performance problems with keyed lists! If you have too many keywords and values, or if your keywords are themselves variables, you can run into serious performance problems. (I cut execution time by two thirds when I replaced global keyed lists with global arrays in a major Tk application; as Mark said, I was "abusing" keyed lists by turning them into large-scale dynamic data storage.) Keyed lists are simply not practical when they get too big or when you try to construct them dynamically. The keyed list functions, being procs, involve more overhead than compiled-in commands, so if you plan very intensive, repeated hits on your data storage, keyed lists would not be a good choice.
If you want the keyed list features but you need more storage, remember that a keyed list can also be stored in an array; you can build large arrays in which each array element is a keyed list.