209: Enums and Enum Sets

by John Cowan (text), Wolfgang Corcoran-Mathe (implementation)

Status

This SRFI is currently in final status. Here is an explanation of each status that a SRFI can hold. To provide input on this SRFI, please send email to srfi-209@nospamsrfi.schemers.org. To subscribe to the list, follow these instructions. You can access previous messages via the mailing list archive.

Abstract

Enums are objects that serve to form sets of distinct classes that specify different modes of operation for a procedure. Their use fosters portable and readable code.

Rationale

Many procedures in many libraries accept arguments from a finite set (usually a fairly small one), or subsets of a finite set to describe one or more modes of operation. Offering a mechanism for dealing with such values fosters portable and readable code, much as records do for compound values, or multiple values for procedures computing several results.

This SRFI provides something related to the enums of Java version 5 and later. These are objects of a type disjoint from all others that are grouped into enum types (called enum classes in Java). In Java, each enum type declares the names and types of values associated with each object, but in this SRFI an enum object has exactly one value; this is useful when translating from C to record the numeric value, but has other uses as well.

In this SRFI, each enum has four properties: the enum type to which it belongs, its name (a symbol), its ordinal (an exact integer), and its value (any object). An enum type provides access to all the enums that belong to it by name or ordinal.

Alternatives

In Lisp-family languages, it is traditional to use symbols and lists of symbols for this purpose. Symbols have at least two disadvantages: they are not “type-safe”, in the sense that a single symbol may be used in more than one logically independent universe of flags; and in Scheme symbols do not have associated values (although in Common Lisp they do).

R6RS enums ameliorate these disadvantages by providing "type-safe" sets, which can be stored more efficiently than general lists, possibly as integers. However, neither enum types nor enum objects are exposed, only enum names and enum sets. This means that R6RS cannot have a procedure that takes an enum-type and returns the enum of the type whose ordinal number is n, nor a procedure that takes an existing enum-type and creates an enum-set containing specified enums from it. Instead, it must use procedures that return a quasi-curried procedure for performing these operations on a specified enum-type. The nearest equivalent to an enum object in the sense of this SRFI is a singleton enum set. To perform an efficient test of enum set membership, it is necessary to use such a singleton, and comparing two such sets for equality involves = rather than eqv?.

In C, enums have names and numeric values, by default consecutive values, but often powers of two or something externally dictated. However, the name is not accessible at runtime, and enum types are not really disjoint from integer types. (In C++, they are statically distinct.)

Enum collections

Enum sets are used to represent multiple enums that belong to the same type. They provide a subset of the operations provided by SRFI 113 general sets.

Specialized mappings from enums to arbitrary values will be described in a future SRFI. Meanwhile either general-purpose hash tables from SRFI 125 or elsewhere, or SRFI 146 mappings, can be used instead.

Specification

R6RS compatibility

This SRFI provides the same procedures as the (rnrs enums) library. In that library, neither enum types nor enum objects are exposed — only enum-sets and the names of enums. (There are no enum values or ordinals.) Some of the R6RS-specific procedures given below operate in those terms and are redundant with other procedures. These are deprecated, and have been marked with [R6RS] and set in small print.

Predicates

(enum-type? obj)

Returns #t if obj is an enum type, and #f otherwise.

(enum? obj)

Returns #t if obj is an enum, and #f otherwise.

(enum-type-contains? enum-type enum)

Returns #t if enum belongs to enum-type, and #f otherwise.

(enum-type-contains? color (enum-name->enum color 'red)) ⇒ #t

(enum-type-contains? pizza (enum-name->enum color 'red)) ⇒ #f

(enum=? enum0 enum1 enum)

Returns #t if all the arguments are the same enum in the sense of eq? (which is equivalent to having the same name and ordinal) and #f otherwise. It is an error to apply enum=? to enums belonging to different enum types.

(enum=? color-red color-blue) ⇒ #f

(enum=? pizza-funghi (enum-name->enum pizza 'funghi)) ⇒ #t

(enum=? color-red (enum-name->enum color 'red) color-blue) ⇒ #f

(enum<? enum0 enum1 enum)

(enum>? enum0 enum1 enum)

(enum<=? enum0 enum1 enum)

(enum>=? enum0 enum1 enum)

These predicates return #t if their arguments are enums whose ordinals are in increasing, decreasing, non-decreasing, and non-increasing order respectively, and #f otherwise. It is an error unless all of the arguments belong to the same enum type.

(enum<? (enum-ordinal->enum color 0) (enum-ordinal->enum color 1))
 ⇒ #t

(enum>? (enum-ordinal->enum color 2) (enum-ordinal->enum color 1)) ⇒ #t

(enum>=? (enum-ordinal->enum color 2)
         (enum-ordinal->enum color 1)
         (enum-ordinal->enum color 1))
 ⇒ #t

Enum type constructor

(make-enum-type list)

Returns a newly allocated enum type containing a fixed set of newly allocated enums. Both enums and enum types are immutable, and it is not possible to create an enum except as part of creating an enum type.

The elements of list are either symbols or two-element lists, where each list has a symbol as the first element and any value as the second element. Each list element causes a single enum to be generated, and the enum's name is specified by the symbol. It is an error unless all the symbols are distinct within an enum type. The position of the element in list is the ordinal of the corresponding enum, so ordinals within an enum type are also distinct. If a value is given, it becomes the value of the enum; otherwise the enum’s value is the same as the ordinal.

The following example enum types will be used in examples throughout this SRFI, with the identifier type-name referring to the enum of type type with name name.

(define color
  (make-enum-type '(red orange yellow green cyan blue violet)))

(define us-traffic-light
  (make-enum-type '(red yellow green)))

(define pizza
  (make-enum-type '((margherita "tomato and mozzarella")
                    (funghi "mushrooms")
                    (chicago "deep-dish")
                    (hawaiian "pineapple and ham"))))

Enum accessors

(enum-type enum)

Returns the enum type to which enum belongs.

(enum-name enum)

Returns the name (symbol) associated with enum.

(enum-ordinal enum)

Returns the ordinal (exact integer) associated with enum.

(enum-value enum)

Returns the value associated with enum.

Enum finders

These procedures use an enum type and one of the properties of an enum to find the enum object.

(enum-name->enum enum-type symbol)

If there exists an enum belonging to enum-type named symbol, returns it; otherwise return #f.

(enum-name (enum-name->enum color 'green)) ⇒ green
(enum-name->enum color 'mushroom) ⇒ #f

(enum-ordinal->enum enum-type exact-integer)

If there exists an enum belonging to enum-type whose ordinal is exact-integer, returns it; otherwise return #f.

(enum-name (enum-ordinal->enum color 3)) ⇒ green
(enum-ordinal->enum color 10) ⇒ #f

Note: There is no way to find an enum by its value, since values need not be unique.

The following convenience procedures provide enum-finding followed by access to a property.

(enum-name->ordinal enum-type symbol)

Returns the ordinal of the enum belonging to enum-type whose name is symbol. It is an error if there is no such enum.

(enum-name->ordinal color 'blue) ⇒ 5

(enum-name->value enum-type symbol)

Returns the value of the enum belonging to enum-type whose name is symbol. It is an error if there is no such enum.

(enum-name->value pizza 'funghi) ⇒ "mushrooms"
(enum-name->value color 'blue) ⇒ 5

(enum-ordinal->name enum-type exact-integer)

Returns the name of the enum belonging to enum-type whose ordinal is exact-integer. It is an error if there is no such enum.

(enum-ordinal->name color 0) ⇒ red
(enum-ordinal->name pizza 3) ⇒ hawaiian

(enum-ordinal->value enum-type exact-integer)

Returns the value of the enum belonging to enum-type whose ordinal is exact-integer. It is an error if there is no such enum.

(enum-ordinal->value pizza 1) ⇒ "mushrooms"

Enum types

(enum-type-size enum-type)

Returns an exact integer equal to the number of enums in enum-type.

(enum-min enum-type)

Returns the enum belonging to enum-type whose ordinal is 0.

(enum-name (enum-min color)) ⇒ red
(enum-name (enum-min pizza)) ⇒ margherita

(enum-max enum-type)

Returns the enum belonging to enum-type whose ordinal is equal to the number of enums in the enum type minus 1.

(enum-name (enum-max color)) ⇒ violet
(enum-name (enum-max pizza)) ⇒ hawaiian

(enum-type-enums enum-type)

Returns a list of the enums belonging to enum-type ordered by increasing ordinal.

(map enum-name (enum-type-enums pizza)) ⇒
 (margherita funghi chicago hawaiian)

(enum-type-names enum-type)

Returns a list of the names of the enums belonging to enum-type ordered by increasing ordinal.

(enum-type-names color)
 ⇒ (red orange yellow green cyan blue violet)

(enum-type-values enum-type)

Returns a list of the values of the enums belonging to enum-type ordered by increasing ordinal.

(enum-type-values pizza)
 ⇒ ("tomato and mozzarella" "mushrooms" "deep-dish" "pineapple and ham")

Enum objects

(enum-next enum)

Returns the enum that belongs to the same enum type as enum and has an ordinal one greater than enum. Returns #f if there is no such enum.

(enum-name (enum-next color-red)) ⇒ orange
(enum-next (enum-max color)) ⇒ #f

(enum-prev enum)

Returns the enum that belongs to the same enum type as enum and has an ordinal one less than enum. Returns #f if there is no such enum.

(enum-name (enum-prev color-orange)) ⇒ red
(enum-prev (enum-min color)) ⇒ #f

Comparators

(make-enum-comparator enum-type)

Returns a SRFI 128 comparator suitable for comparing enums that belong to enum-type. The comparator contains both an ordering predicate and a hash function, and orders enums based on their ordinal values.

(define pizza-comparator (make-enum-comparator pizza))

(comparator-hashable? pizza-comparator) ⇒ #t

(comparator-test-type pizza-comparator pizza-funghi) ⇒ #t

(<? pizza-comparator pizza-margherita pizza-chicago) ⇒ #t

Enum set constructors

(enum-empty-set enum-type)

Returns an empty enum set that can contain enums of the type enum-type.

(enum-type->enum-set enum-type)

Returns an enum set containing all the enums that belong to enum-type.

(define color-set (enum-type->enum-set color))

(define pizza-set (enum-type->enum-set pizza))

(every (lambda (enum)
         (enum-set-contains? pizza-set enum))
       (enum-type-enums pizza))
 ⇒ #t

(enum-set-map->list enum-name color-set)
 ⇒ (red orange yellow green cyan blue violet)

(enum-set enum-type enum)

Returns an enum set that can contain enums of the type enum-type and containing the enums. It is an error unless all the enums belong to enum-type.

(enum-set-contains? (enum-set color color-red color-blue) color-red)
 ⇒ #t
(enum-set-contains? (enum-set color color-red color-blue) color-orange)
 ⇒ #f

(list->enum-set enum-type list)

Returns an enum set with the specified enum-type that contains the members of list. It is an error unless all the members are enums belonging to enum-type.

(list->enum-set (enum-type-enums pizza))
 = (enum-type->enum-set pizza)

(enum-set-contains? (list->enum-set pizza (list pizza-funghi pizza-chicago))
                    pizza-funghi)
 ⇒ #t

(enum-set-projection enum-type-or-set enum-set)

If enum-type-or-set is an enum set, its enum type is extracted and used; otherwise, the enum type is used directly. Returns an enum set containing the enums belonging to the enum type that have the same names as the members of enum-set, whose enum type need not be not the same as the enum-type. It is an error if enum-set contains an enum that does not correspond by name to an enum in the enum type of enum-type-or-set.

(enum-set-projection us-traffic-light
            (enum-set color color-red color-green color-blue))
 = (enum-set us-traffic-light
             us-traffic-light-red us-traffic-light-green)

(enum-set-copy enum-set)

Returns a copy of enum-set.

[R6RS] (make-enumeration symbol-list)

Creates a newly allocated enum type. The names are the members of symbol-list, and they appear in the enum set in the order given by the list. The values are the same as the names. Then an enum set containing all the enums of this enum type is newly allocated and returned. The enum type can be retrieved with enum-set-type.

[R6RS] (enum-set-universe enum-set)

Retrieves the enum type of enum-set, and returns a newly allocated enum set containing all the enums of the enum type.

[R6RS] (enum-set-constructor enum-set)

Returns a procedure that accepts one argument, a list of symbols. This procedure returns a newly allocated enum set containing the enums whose names are members of the list of symbols. It is an error if any of the symbols is not the name of an enum in the enum type associated with enum-set.

Enum set predicates

(enum-set? obj)

Returns #t if obj is an enum-set and #f otherwise.

(enum-set-contains? enum-set enum)

Returns #t if enum is a member of enum-set. It is an error if enum does not belong to the same enum type as the members of enum-set.

(enum-set-contains? color-set color-blue) ⇒ #t
(enum-set-contains? (enum-set-delete! color-set color-blue) color-blue) ⇒ #f

[R6RS] (enum-set-member? symbol enum-set)

Returns #t if symbol is the name of a member of enum-set. It is an error if symbol is not the name of an enum belonging to the enum type of enum-set.

(enum-set-empty? enum-set)

Returns #t if enum-set is empty, and #f otherwise.

(enum-set-empty? color-set) ⇒ #f
(enum-set-empty? (enum-set-delete-all! color-set (enum-set->enum-list color-set)))
 ⇒ #t

(enum-set-disjoint? enum-set1 enum-set2)

Returns #t if enum-set1 and enum-set2 do not have any enum objects in common, and #f otherwise. It is an error if the members of the enum sets do not belong to the same type.

(define reddish
  (list->enum-set (map (lambda (name)
                         (enum-name->enum color name))
                       '(red orange))))

(define ~reddish
  (list->enum-set (map (lambda (name)
                         (enum-name->enum color name))
                       '(yellow green cyan blue violet))))

(enum-set-disjoint? color-set reddish) ⇒ #f
(enum-set-disjoint? reddish ~reddish) ⇒ #t

Note that the following three procedures do not obey the trichotomy law, and cannot be used to define a comparator.

(enum-set=? enum-set-1 enum-set-2)
(enum-set<? enum-set-1 enum-set-2)
(enum-set>? enum-set-1 enum-set-2)
(enum-set<=? enum-set-1 enum-set-2)
(enum-set>=? enum-set-1 enum-set-2)

Returns #t if the members of enum-set-1 are the same as / a proper subset of / a proper superset of / a subset of / a superset of enum-set-2. It is an error if the members of the enum sets do not belong to the same type.

(enum-set=? color-set (enum-set-copy color-set)) ⇒ #t
(enum-set=? color-set reddish) ⇒ #f
(enum-set<? reddish color-set) ⇒ #t
(enum-set>? reddish color-set) ⇒ #f
(enum-set<=? reddish color-set) ⇒ #t
(enum-set>=? reddish color-set) ⇒ #f

(enum-set-subset? enum-set-1 enum-set-2)

Returns #t if the set of the names of the elements of enum-set-1 is a subset of the set of the names of the elements of enum-set-2. Otherwise returns #f. Note that enum-set-1 and enum-set-2 can be of different enum types.

(enum-set-subset? (enum-set color red blue)
                  (enum-set color red green blue)) ⇒ #t

(enum-set-subset? (enum-set us-traffic-light red green)
                  (enum-set color red green blue)) ⇒ #t

(enum-set-any? pred enum-set)
(enum-set-every? pred enum-set)

Returns #t if any/every application of proc to the elements of enum-set returns true, and #f otherwise.

(enum-set-any? (lambda (e) (eqv? 'green (enum-name e)))
               color-set)
 ⇒ #t
(enum-set-any? (lambda (e) (eqv? 'green (enum-name e)))
               reddish)
 ⇒ #f
(enum-set-every? (lambda (e) (eq? 'green (enum-name e)))
                 color-set)
 ⇒ #f
(enum-set-every? (lambda (e) (string? (enum-value e)))
                 pizza-set)
 ⇒ #t

Enum set accessors

(enum-set-type enum-set)

Returns the enum type associated with enum-set.

[R6RS] (enum-set-indexer enum-set)

Returns a procedure that accepts one argument, a symbol. When this procedure is called, if the symbol is the name of an enum in the enum type associated with enum-set, then the ordinal of that enum is returned. Otherwise, #f is returned.

Enum set mutators

These procedures come in pairs. Procedures whose names end in ! are linear-update: that is, they may or may not modify their enum-set argument, and any existing references to it are invalidated. Other procedures are functional and return a newly allocated modified copy of their enum-set argument.

(enum-set-adjoin enum-set enum)
(enum-set-adjoin! enum-set enum)

Returns an enum set that contains the members of enum-set and the enums. It is an error if the members of the result do not all belong to the same enum type.

(define reddish+blue
  (enum-set-adjoin! (enum-set-copy reddish) color-blue))

(enum-set<? reddish reddish+blue) ⇒ #t
(enum-set-contains? reddish+blue color-blue) ⇒ #t

(enum-set-delete enum-set enum)
(enum-set-delete! enum-set enum)

Returns an enum set that contains the members of enum-set excluding the enums.

(define no-blue
  (enum-set-delete! (enum-set-copy color-set) color-blue))

(enum-set<? no-blue color-set) ⇒ #t
(enum-set-contains? no-blue color-blue) ⇒ #f

(enum-set-delete-all enum-set list)
(enum-set-delete-all! enum-set list)

Returns an enum set that contains the members of enum-set excluding the members of list.

(define empty-colors
  (enum-set-delete-all! (enum-set-copy color-set)
                        (enum-set->enum-list color-set)))

(enum-set<? empty-colors reddish) ⇒ #t
(enum-set-empty? empty-colors) ⇒ #t

Enum set operations

(enum-set-size enum-set)

Returns the number of elements in enum-set.

(enum-set-size (enum-set color color-red color-blue)) ⇒ 2

(enum-set->enum-list enum-set)
[R6RS] (enum-set->list enum-set)

Returns a list containing the members of enum-set, whereas the set->enum-list procedure returns a list containing the names of the members of enum-set. In either case, the list will be in increasing order of the enums.

(map enum-name (enum-set->enum-list reddish)) ⇒ (red orange)
(list->enum-set (enum-set->enum-list color-set)) ⇒ color-set

(enum-set-count pred enum-set)

Returns an exact integer, the number of elements of enum-set that satisfy pred.

(enum-set-count (lambda (e) (> (enum-ordinal e) 3))
                color-set)
 ⇒ 3

(enum-set-filter pred enum-set)
(enum-set-remove pred enum-set)

Returns an enum set containing the enums in enum-set that satisfy / do not satisfy pred.

(enum-set-map->list proc enum-set)

Invokes proc on each member of enum-set in increasing ordinal order. The results are made into a list and returned.

(enum-set-map->list enum-name
                    (enum-set-filter (lambda (e) (> (enum-ordinal e) 3))
                                     color-set))
 ⇒ '(cyan blue violet)

(enum-set-for-each proc enum-set)

Invokes proc on each member of enum-set in increasing ordinal order and discards the rest. The result is an unspecified value.

(let ((s ""))
  (begin
   (enum-set-for-each (lambda (e)
                        (set! s (string-append s (enum-value e) " ")))
                      (enum-set pizza pizza-margherita pizza-chicago))
   s))
   ⇒ "tomato and mozzarella deep-dish "

(enum-set-fold proc nil enum-set)

The current state is initialized to nil, and proc is invoked on each element of enum-set in increasing ordinal order and the current state, setting the current state to the result. The algorithm is repeated until all the elements of enum-set have been processed. Then the current state is returned.

(enum-set-fold cons '() color-set)
 = (reverse (enum-set->enum-list color-set))

Enum set logical operations

These procedures come in pairs. Procedures whose names end in ! are linear-update: that is, they may or may not modify their enum-set argument(s), and any existing references to them are invalidated. Other procedures are functional and return a newly allocated modified copy of their enum-set argument.

(enum-set-complement enum-set)
(enum-set-complement! enum-set)

Returns an enum set that contains the elements of the enum type of enum-set that are not members of enum-set.

(enum-set-union enum-set-1 enum-set-2)
(enum-set-union! enum-set-1 enum-set-2)

Returns an enum set containing all the elements of either enum-set-1 or enum-set-2. It is an error if all the elements of the result do not belong to the same enum type.

(enum-set-map->list enum-name
                    (enum-set-union! (enum-set color color-orange)
                                     (enum-set color color-blue)))
 ⇒ (orange blue)

(enum-set=? color-set (enum-set-union! reddish ~reddish)) ⇒ #t

(enum-set-intersection enum-set-1 enum-set-2)
(enum-set-intersection! enum-set-1 enum-set-2)

Returns an enum set containing all the elements that appear in both enum-set-1 and enum-set-2. It is an error if all the elements of the result do not belong to the same enum type.

(enum-set-empty? (enum-set-intersection! reddish ~reddish))
 ⇒ #t

(enum-set-difference enum-set-1 enum-set-2)
(enum-set-difference! enum-set-1 enum-set-2)

Returns an enum set containing the elements of enum-set-1 but not enum-set-2. It is an error if all the elements of the result do not belong to the same enum type.

(enum-set=? ~reddish (enum-set-difference! color-set reddish))
 ⇒ #t

(enum-set-xor enum-set-1 enum-set-2)
(enum-set-xor! enum-set-1 enum-set-2)

Returns an enum set containing all the elements that appear in either enum-set-1 or enum-set-2 but not both. It is an error if all the elements of the result do not belong to the same enum type.

(enum-set=? color-set (enum-set-xor! reddish ~reddish))
 ⇒ #t

(enum-set-empty? (enum-set-xor! reddish reddish)) ⇒ #t

Syntax

(define-enum type-name (name-value ...)  constructor-syntax)

(define-enumeration type-name (name-value ...)  constructor-syntax)

These macros allocate a newly created enum type and provide two macros for constructing its members and sets of its members. They are definitions and can appear anywhere any other definition can appear. Each <name-value>  is either a symbol naming an enum or a two-element list specifying the name and value of an enum.

<Type-name> is an identifier that is bound to a macro. When <type-name> is invoked as (<type-name> <symbol>), it returns the enum named <symbol> in the case of define-enum or the symbol itself in the case of define-enumeration. If the symbol does not name any enum of the enum-type, an error is signaled.

<Constructor-syntax> is an identifier that is bound to a macro that, given any finite sequence of the names of enums, possibly with duplicates, expands into an expression that evaluates to an enum set of those enums.

If any of the symbols does not name any enum of the enum-type, an error is signaled.

Implementation

The implementation of this SRFI is in its repository (Github).

Acknowledgements

The Java Enum class and the R6RS (rnrs enums) library provide the main inspirations for this SRFI; acknowledgement is due to their authors. Thanks also to the participants on the SRFI mailing list.

Copyright

© 2020 John Cowan, Wolfgang Corcoran-Mathe.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


Editor: Arthur A. Gleckler