Implementing enumerated types

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