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
cherryGives you sweet little mappers symbol -> number, and back. No error checking, but no global variables either - and no backslashes in code ;-)JMN 2005-11-18Here'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 valuesThis 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 & numberi.e it is not immediately obvious what 'enum days $x' returnsUPDATE. 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
