Where did my Enum go?
Enum is probably one of the things I miss most in Ruby. Not sure why - but as a fairly common design pattern I expected to see it more than I do in Ruby’s Stdlib (or maybe even Core) and in Ruby / Rails implementations around.
Enum (or Factor if you will) is basically a set of values called members (or elements) that act as a constant and represent some sort of a numeric (mostly integer) value. It is mainly used to represent a string constant in a numeric way, allowing easier serialization of data representation and avoiding recurring typs of long / annoying strings.
Java, C#, and even our beloved neighbor Python - all provide some kind of an implementation to Enum as part of their toolbelt, some databases like (MySQL)[http://dev.mysql.com/doc/refman/5.0/en/enum.html], Postgres provide a specific implementation “by the book” for enum types but some others like SQL-Server (yes, Microsoft again) provide some black magic to achieve an Enum representation:
But, a decent Enum is still missing in Ruby.
Quick draw Enum in Ruby
If you ignore all the fancy words around the Enum definition, you are probably saying to yourself - “Eh, it’s a hash”.
The most simple way is to create a frozen hash constant that will hold all your constants:
1 2 3 4 5 6 7 8
testing it out:
This implementation is rather simple and convenient - but not too easy to maintain and carries absolutely no syntactic sugar.
Note that if you want to access the numeric value via a given constant (ex:
Canvas::Colors[:black]) you’ll need to implement external access methods for the frozen hash. Another problem I have with this implementation is that it is too specific, if you want an other class
to use the
COLORS “enum”, you’ll need to redefine it again.
Reusable Module Enum
The Reusable module implementation is very similar to the implementation that you’ll find in C#, Python and Java - a free module that allows the enum to be re-used in multiple classes and is basically all about defining constants:
1 2 3 4 5 6 7 8 9 10 11 12 13
This form allows code re-using of course, but still lacks the ability to access the enum members by both numeric index and the constant.
Enum by method
Before reaching out to solve the dual access problem (isn’t it annoying to declare constants manually?) lets add some syntactic sugar to the constant definition process.
1 2 3 4 5 6 7 8 9 10 11 12 13
Now we are getting closer. We can’t use this syntactic sugar anywhere in our app to define enums like the big boys do, but it still doesn’t solve our dual access problem.
Enum with dual-access
The feature that almost all the Enum implementations I presented above are missing is the ability to access an enum member by either its value or by name.
The following implementation (original from here) suggests a more robust module based implementation, it adds an object implementation for each enum member to store all member attributes (index for example) and
extends the enum base class with the
Enumerable module capabilities.
I used this base class and added some more attributes to the
Enum::Member class to also store the actual value and allow dual access to both the key and the value of the member:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
Lets focus on the
Enum# method in line 49:
1 2 3 4 5 6 7 8 9
This method allows the dual-access mode I discussed before when doing the following
- When getting a
Numericvalue as a parameter it checks it against any memeber’s
- When getting a string it compares it to the
This implementation stores the member list in an array member named
@members and delegates manually all the
Enumerable methods to this array member.
Seems like we can simply inherit from
Array and drop all these delegation methods.
Set based Enum
Set implements a collection of unordered values with no duplicates. By using
Set as a parent we inherit its ability to
traverse members, we don’t need to implement and manage delegation to an array member and we don’t need to create constants (I’ll discuss that later in this article):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
First, I’ll focus on the
#populate method. This method is fired when
#initialize finishes and appends all members applied to the Enum set.
Line 50 is where all the magic happens - it creates a new
Enum::Member and adds it to the set, unless it already exists.
Wait! Aren’t set members already unique by default?
Yes, they are. But this implementation runs into a Ruby brick wall. The ruby
Set keeps an internal hash (named
@hash surprisingly) that holds the member as keys and sets a value of true for each one when they are entered.
Set#include? is delegated to
Hash that on its turn - runs a native C code that compares objects directly.
In our case it will result in something like this:
Which, no matter if both members have the same attributes, will return false.
Set#include? does not exercise the
Comparable module convenience I included in
Enum::Member, so we need to do a comparison on our own.
And why didn’t you use Constants?
It is easy and smart to use constants when you can take care of any naming conventions and constant name limitations that Ruby introduces, in our case
Enum::Member to initialize with any kind of string, even ones that don’t pass the constant name restrictions, some examples are:
1 2 3 4 5 6 7 8
Since we don’t know or even want to restrict our enum value names, dropping the constant convention out of the loop seemed like the right thing to do.
Update: A clean, simple and perfect Hash
After this post was published, a talked to my friend Daniel about the
Set implementation. He suggested the the
Set usage is
not required since that
#include? bug causes me to write the presence test myself and that anyway, I traverse the members myself so the
Set has no meaning.
he later came up with this suggestion:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
This solution, not only answer the dual access requirement - but it drops the need to use an internal
Enum::Member inclusion. Definitely a winner
Ruby misses a real enum implementation. I would be happy to see an official one come out in one of the next releases. Until then the
Hash implementation is something I will be using.
Source available here, fork away.