Is there a mechanism whereby I can create constants or even better, enumerated types within Tcl?
KBK: In Tcl, everything is a string, so you can simply use your names as constants. If you want to assign numeric values, you can do something like:
set fruits { apple blueberry cherry date elderberry } set i 0 foreach fruit $fruits { set fruit_number($fruit) $i incr i } proc fruit_to_number { fruit } { variable fruit_number if { [catch { set fruit_number($fruit) } number] } { return -code error "$fruit: no such fruit" } else { return $number } } proc number_to_fruit { number } { variable fruits if { [catch { if { $number < 0 || $number >= [llength $fruits] } { error {out of range} lindex $fruits $number } fruit] } { return -code error "no fruit with number $number" } else { return $fruit } }
DKF adds: I reckon it is easier to just load up a pair of arrays to map in each direction, and then use them directly, which we can dress up like this...
proc makeEnum {type identifiers} { upvar #0 ${type}_number a1 number_${type} a2 set n 0 foreach id $identifiers { incr n set a1($id) $n set a2($n) $id } proc ${type}_to_number $type " upvar #0 ${type}_number ary if {\[catch {set ary(\$$type)} num\]} { return -code error \"unknown $type \\\"\$$type\\\"\" } return \$num " proc number_to_${type} {number} " upvar #0 number_${type} ary if {\[catch {set ary(\$number)} $type\]} { return -code error \"no $type for \\\"\$number\\\"\" } return \$$type " } makeEnum fruit {apple blueberry cherry date elderberry}
kruzalex An alternative using subst command:
uplevel 1 [subst -nocommands { proc ${type}_to_number $type { upvar #0 ${type}_number ary if {[catch {set ary($$type)} num]} { return -code error "unknown $type \\\"$$type\\\"" } return \$num } }] uplevel 1 [subst -nocommands { proc number_to_${type} {number} { upvar #0 number_${type} ary if {[catch {set ary(\$number)} $type]} { return -code error "no $type for \\\"\$number\\\"" } return \$$type } }]
KBK: I like the 'makeEnum' syntax, but [lindex] is faster than an array search, at least in Tcl 8.3.2. Also, was there a reason you switched from zero-based to one-based indexing? (DKF: no, it just came out that way. :^)
Consider the alternative implementation:
proc makeEnumKBK {type identifiers} { upvar #0 ${type}_number a1 set n 0 foreach id $identifiers { set a1($id) $n lappend list $id incr n } proc ${type}_to_number $type " upvar #0 ${type}_number ary if {\[catch {set ary(\$$type)} num\]} { return -code error \"unknown $type \\\"\$$type\\\"\" } return \$num " proc number_to_${type} {number} " if { \[catch { if { \$number < 0 || \$number >= [llength $list] } { error {out of range} } lindex [list $list] \$number } $type\] } { return -code error \"no $type for \\\"\$number\\\"\" } return \$$type " } makeEnumKBK froot {apple blueberry cherry date elderberry}
Running comparative timings on my machine shows that the array+list implementation is significantly faster, mostly because it avoids the cost of the [upvar]:
Time (us; 550 MHz PIII) Action Two arrays Array + list ------------------------------------------------------------ Convert enum to number 23 23 Try to convert nonexistent enum to number 80 81 Convert number to enum 23 13 Try to convert out-of-range number to enum 79 60 Try to convert non-number to enum 80 65 ------------------------------------------------------------
RS has this variation, based on a little codelet in Complex data structures:
proc makeEnum {name values} { interp alias {} $name: {} lsearch $values interp alias {} $name@ {} lindex $values } % makeEnum fruit {apple blueberry cherry date elderberry} fruit@ % fruit: date 3 % fruit@ 2 cherry
Gives you sweet little mappers symbol -> number, and back. No error checking, but no global variables either - and no backslashes in code ;-)
JMN 2005-11-18
Here's a packaged up Tcl 8.5+ example of how the above might be implemented using the namespace ensemble command. Save as enum-1.0.tm and place it on your module-path. (2006-08-12 later versions, with bitmask support, here: http://vectorstream.com/tcl/packages/docs/enum/ )
package provide enum [namespace eval enum { set commands {create destroy types values} namespace export {*}$commands namespace ensemble create variable version set version 1.0 }] proc ::enum::types {} { set list [namespace export] foreach c $::enum::commands { set posn [lsearch $list $c] set list [lreplace $list $posn $posn] } return $list } proc ::enum::create {type identifiers} { if {[lsearch $::enum::commands $type] >= 0} { error "cannot create enumerated type for keyword '$type'" } upvar #0 ::enum::${type}_number a1 #obliterate any previous enum for this type catch {unset a1} catch {unset ::enum::number_$type} set n 0 foreach id $identifiers { set a1($id) $n incr n } #store list for use by 'values' command set ::enum::number_$type $identifiers proc ::enum::$type {to {key ""}} [string map [list @type@ [list $type] @ids@ [list $identifiers]] { if {![string length $key]} { set key $to set to [expr {[string is integer -strict $key]?"nam":"num"}] } switch -- [string range $to 0 2] { nam { #list may be large - let's not substitute it into the proc body more than once. #(!todo - compare performance) set list @ids@ if {[catch { if {$key < 0 || $key >= [llength $list] } { error {out of range} } lindex $list $key } val]} { return -code error "no @type@ for '$key'" } return $val } num { if {[catch {set ::enum::@type@_number($key)} val]} { return -code error "unknown @type@ '$key'" } } default { return -code error "unknown conversion specifier '$to'" } } return $val }] namespace export $type } proc ::enum::values {type} { return [set ::enum::number_$type] } proc ::enum::destroy {type} { unset ::enum::${type}_number unset ::enum::number_${type} rename ::enum::$type {} set posn [lsearch [namespace export] $type] namespace export -clear {*}[lreplace [namespace export] $posn $posn] } usage e.g %package require enum %enum create days {mon tue wed thu fri sat sun} %enum days tue 1 %enum days 3 thu %enum create test {a b c} %enum types days test %enum values test a b c %enum destroy test %enum test 1 unknown or ambiguous subcommand "test": must be create, days, destroy, types or values
This system does however suffer from a slight readability issue by the use of a single accessor mechanism instead of separate functions for retrieval by name & number
i.e it is not immediately obvious what 'enum days $x' returns
UPDATE. I've changed the generated enum command to take an optional 'conversion specifier' prior to the key. The above examples still work - but now you have the option to do the following for (hopefully) clarity.
%enum days number fri 4 %enum days num sat 5 %enum days name 6 sun %enum days nam 3 thu %enum days blah 5 unknown conversion specifier 'blah'
NEM Enumerated types can be generalised into Algebraic Types of the "sum of product" form. Thus, the fruit example can be written as:
% datatype define Fruit = Apple | Blueberry | Cherry | Date | Elderberry Fruit % set fruit [Blueberry] Blueberry % datatype match $fruit { case [Apple] -> { puts apple } case [Blueberry] -> { puts blueberry } default -> { puts "no match" } } blueberry