The Holidays gave me some time to go back to work on another of my little libraries. Here is the result.
Enumerated Types in Common Lisp
Several programming languages have the notion of
enumerated
type, e.g., C/C++
enum
.
Common
Lisp can substitute the notion of enumerated type using the
member
type specifiers.
(deftype colors () '(member red green blue))
This is sufficient for the annotation purposes of traditional
Common Lisp, but it falls short of the standard uses
that are expected of enumerated types in C/C++ and other languages.
Moreover, languages like Java now provide very
structured
enumerated types.
Consider the following Java enumerated type, which provides an
idiomatic way to express certain language processing functionalities:
public enum Operation {
PLUS { double eval(double x, double y) { return x + y; } },
MINUS { double eval(double x, double y) { return x - y; } },
TIMES { double eval(double x, double y) { return x * y; } },
DIVIDE { double eval(double x, double y) { return x / y; } };
// Do arithmetic op represented by this constant
abstract double eval(double x, double y);
}
It is not difficult to emulate such methods in
Common
Lisp using
EQL
specializers and using symbols as
enumerated type tags.
On the other hand, C/C++ use of enumerated types as
numeric integer constants, as in
enum numbers {ONE,
TWO,
FORTY_TWO = 42,
FORTY_THREE
};
can easily be emulated in
Common Lisp by using
appropriate
defconstant
's; alas, this breaks the use of
case
and similar constructs.
Common Lisp programmers have, needless to say,
come up with several versions of
enumerated types. Most
available definitions provide a
DEFENUM
or
DEF-ENUM
macro which provides functionalities
similar to C/C++ enumerated types, while adding a
switch
or
select
macros as work-around the
case
problem. A typical rendition of the C/C++
enum
just
shown could be rendered as
(def-enum numbers ONE TWO (FORTY-TWO 42) FORTY-THREE)
In the best
Common Lisp N.I.H. tradition, yet
another
n DEFENUM
macro is necessary.
The DEFENUM Library
The present library, unimaginatively named
DEFENUM, provides a new
DEFENUM
macro
that merges most of the functionalities provided elsewhere. The
objective is, as always,
to make the simple things simple and the complex ones possible (and
possibly, simple as well).
The simplest enumerated types are represented as expected:
(defenum season (spring summer fall winter))
This defines an enumerated type (the macro expands - also - into a
deftype
) with the four tags indicating the seasons. The
type checks work as expected, and the C/C++ behavior is also
reproduced, as the following shows:
cl-prompt> (typep 'fall 'season)
T
cl-prompt> (+ fall winter)
5
The library provides also a number of facilities to handle enumerated
types.
cl-prompt> (season-p 'spring)
T
cl-prompt> (season-p winter) ; No quote.
T
The function
season
accesses the individual tags.
cl-prompt> (season winter)
WINTER
Tags can be handled as lists.
cl-prompt> (tags 'season)
(SPRING SUMMER FALL WINTER)
Other functions are available as well.
cl-prompt> (loop for season in (tags 'season)
for s-id from spring
do (format t "~S is followed by ~S~%"
season
(tag-of 'season (mod (1+ s-id) 4))))
SPRING is followed by SUMMER
SUMMER is followed by FALL
FALL is followed by WINTER
WINTER is followed by SPRING
NIL
cl-prompt> (previous-enum-tag 'season 'fall)
SUMMER
Enumerated types live in their own namespace.
cl-prompt> (find-enum 'season)
#<ENUM SEASON (SPRING SUMMER FALL WINTER)>
Simple and Structured Enumerated Types
The
season
enumerated type is
simple: nothing
particularly complicated. On the other hand, Java enumerated types
show that is possible to extend the notion of enumerated type in an
interesting way, by introducing the notion of
structured
enumerated type.
Two interesting examples in Java
[JE5] are
the
Planet
and the
Operation
enumerated
types.
public enum Planet {
MERCURY (3.303e+23, 2.4397e6),
VENUS (4.869e+24, 6.0518e6),
EARTH (5.976e+24, 6.37814e6),
MARS (6.421e+23, 3.3972e6),
JUPITER (1.9e+27, 7.1492e7),
SATURN (5.688e+26, 6.0268e7),
URANUS (8.686e+25, 2.5559e7),
NEPTUNE (1.024e+26, 2.4746e7),
PLUTO (1.27e+22, 1.137e6);
private final double mass; // in kilograms
private final double radius; // in meters
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
}
public double mass() { return mass; }
public double radius() { return radius; }
// universal gravitational constant (m3 kg-1 s-2)
public static final double G = 6.67300E-11;
public double surfaceGravity() {
return G * mass / (radius * radius);
}
public double surfaceWeight(double otherMass) {
return otherMass * surfaceGravity();
}
}
A program that exercises the
Planet
enumerated type is
the following (the units are unimportant):
$ cat Planet.java
public enum Planet {
...
public static void main(String[] args) {
double earthWeight = Double.parseDouble(args[0]);
double mass = earthWeight/EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf("Your weight on %s is %f%n",
p, p.surfaceWeight(mass));
}
}
$ java Planet 175
Your weight on MERCURY is 66.107583
Your weight on VENUS is 158.374842
Your weight on EARTH is 175.000000
Your weight on MARS is 66.279007
Your weight on JUPITER is 442.847567
Your weight on SATURN is 186.552719
Your weight on URANUS is 158.397260
Your weight on NEPTUNE is 199.207413
Your weight on PLUTO is 11.703031
DEFENUM provides
structured enumerated
types as well. The Java
Planet
enumerated type and the
test program above are rendered in
Common Lisp as
follows:
(defconstant g 6.67300D-11)
(defenum (planet (:initargs (mass radius)))
((MERCURY (3.303D+23 2.4397D6))
(VENUS (4.869D+24 6.0518D6))
(EARTH (5.976D+24 6.37814D6))
(MARS (6.421D+23 3.3972D6))
(JUPITER (1.9D+27 7.1492D7))
(SATURN (5.688D+26 6.0268D7))
(URANUS (8.686D+25 2.5559D7))
(NEPTUNE (1.024D+26 2.4746D7))
(PLUTO (1.27D+22 1.137D6))
)
((mass 0.0d0 :type double-float)
(radius 0.0d0 :type double-float)
)
(:documentation "The Planet Enum.")
(:method surface-gravity ((p planet))
(* g (/ mass (* radius radius))))
(:method surface-weight ((p planet) other-mass)
(* other-mass (surface-gravity p)))
)
Note the definition of methods that will be specialized on the tags of
the enumeration. With the definition above, the test Java program can
be rendered, as an example, as follows:
cl-prompt> (let* ((earth-weight 175)
(mass (/ earth-weight (surface-gravity 'earth)))
)
(dolist (p (tags 'planet))
(format t "Your weight on ~S is ~F~%"
(tag-name p)
(surface-weight p mass)))
Your weight on MERCURY is 66.10758266016366
Your weight on VENUS is 158.37484247218296
Your weight on EARTH is 174.99999999999997
Your weight on MARS is 66.27900720649754
Your weight on JUPITER is 442.84756696175464
Your weight on SATURN is 186.55271929202414
Your weight on URANUS is 158.39725989314937
Your weight on NEPTUNE is 199.20741268219015
Your weight on PLUTO is 11.703030772485283
tag-name
is necessary because the
print-object
method for
structured tags is not
too extreme; it
could be made to print the tag name.
Of course, all the other functions presented before work as expected.
cl-prompt> (planet-p 'venus)
T
cl-prompt> (planet-p 'vulcan)
NIL
The Java
Operation
example shows how to define methods
that are actually specialized on tags (the rationale is to avoid
writing error-prone non-object-oriented
switch
statements. The Java
Operation
is the following:
public enum Operation {
PLUS { double eval(double x, double y) { return x + y; } },
MINUS { double eval(double x, double y) { return x - y; } },
TIMES { double eval(double x, double y) { return x * y; } },
DIVIDE { double eval(double x, double y) { return x / y; } };
// Do arithmetic op represented by this constant
abstract double eval(double x, double y);
public static void main(String args[]) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n", x, op, y, op.eval(x, y));
}
}
This form of Java enum type definition allows for the direct
association of methods to tags. In
Common Lisp this
is nothing more than
EQL
specialized methods, and
DEFENUM directly provides for this idiom. Actually
there are two equivalent forms for it.
(defenum operation
(PLUS MINUS TIMES DIVIDE)
()
(:method evaluate ('plus x y) (+ x y)) ; Note the 'quote' shorthand.
(:method evaluate ('minus x y) (- x y))
(:method evaluate ('times x y) (* x y))
(:method evaluate ('divide x y) (/ x y))
)
... or, more similar to the original Java idiom ...
(defenum operation
((PLUS () (:method evaluate (x y) (+ x y))) ; Note the 'no tag argument' shorthand.
(MINUS () (:method evaluate (x y) (- x y)))
(TIMES () (:method evaluate (x y) (* x y)))
(DIVIDE () (:method evaluate (x y) (/ x y)))
))
The test program works as expected.
cl-prompt> (let ((x 4) (y 2))
(dolist (op (tags 'operation))
(format t "~S ~S ~S = ~S~%" x (tag-name op) y (evaluate op x y))))
4 PLUS 2 = 6
4 MINUS 2 = 2
4 TIMES 2 = 8
4 DIVIDE 2 = 2
NIL
Therefore
DEFENUM allows for all Java enumerated
types idioms.
Having Your Cake...
Java enumerated types cannot be used in the way C/C++ enum types
are (or, at least, not directly). This is because Java has only
structured enumerated types.
DEFENUM lets you eat your cake.
Consider the following (contrived) C++ program.
// rgb.cc --
#include <iostream>
using namespace std;
enum rgb {
RED = 0xff0000,
GREEN = 0x00ff00,
BLUE = 0x0000ff
};
int
mix_colors(enum rgb c1, int c2, int c3) { return c1 | c2 | c3; }
int
main() {
enum rgb basic_color = GREEN;
cout << "The basic color chosen is: " << basic_color << endl;
cout << "WHITE is: " << (RED | GREEN | BLUE) << endl;
cout << "WHITE is also: " << mix_colors(RED, GREEN, BLUE) << endl;
}
// end of file -- rgb.cc --
DEFENUM allows to mix and match
simple
and
structured enumerated types, as the following (again,
contrived) example shows.
cl-prompt> (defenum (colors (:initargs (r g b)))
((red #xff0000 (255 0 0) (:method mix (c2 c3) (logior red c1 c2)))
(green #x00ff00 (0 255 0) (:method mix (c2 c3) (logior green c1 c2)))
(blue #x0000ff (0 0 255) (:method mix (c2 c3) (logior blue c1 c2)))
)
((r 0 :type (integer 0 255))
(g 0 :type (integer 0 255))
(b 0 :type (integer 0 255))
)
(:documentation "The Colors Enum."))
#<ENUM COLORS (RED GREEN BLUE)>
cl-prompt> (format t "The color WHITE is ~D~%" (mix red green blue)) ; Yes, this works as is!
The color WHITE is 16777215
cl-prompt> (mapcar #'colors-r (tags 'colors))
(255 0 0)
The C/C++ style numeric tag can be mixed with structured enumerated
types.
Now you can eat your cake.
Not that you
have to, but it is nice to know you can.
Final Remarks
The
DEFENUM library is inspired by Java 5.0
'enum' classes and it allows you to build both
simple and
structured enumerated types in
Common Lisp. It offers
all the facilities of the Java version, while also retaining the C/C++
'enum tags are integers' feature.
The use of the
DEFENUM enumeration types has some
limitations, due, of course, to
Common Lisp slack
typing and a few implementation choices.
References
The usual ones, plus searches of '
common lisp defenum def-enum'.
[JE5] Java Online Documentation,
Enums,
link.
Project site
DEFENUM is hosted at
http://defenum.sourceforge.net.
Enjoy!
(cheers)