Enumerations and Ruby

Enumerations are an elegant way to make your code more readable. Unfortunately, Ruby doesn’t have a built-in enum type. However, if you only want to use an enumeration like a set of constants logically grouped, it’s super easy to do. You only have to create a class and define constants in it :

class Color
  BLUE=1
  RED=2
  GREEN=3
  YELLOW=4
  ORANGE=5
  PURPLE=6
end
paint_the_car(Color::YELLOW)

If you need a more elaborate enum, that is, if you want to iterate through every items like if it was a collection, you’ll have some more work to do (but not that much) :

class Color
    def Color.add_item(key,value)
        @hash ||= {}
        @hash[key]=value
    end
    def Color.const_missing(key)
        @hash[key]
    end
    def Color.each
        @hash.each {|key,value| yield(key,value)}
    end
    Color.add_item :BLUE, 1
    Color.add_item :RED, 2
    Color.add_item :YELLOW, 3
end
#That's it! We can now use our enum :
my_color = Color::RED if some_condition
#And we can loop
Color.each do |key,value|
  do_something_with_value(value)
end

Basically, we just used a hash in our class to make our enum behave like a collection.

  1. The add_item method is used to fill the hash and is called inside the class because we don’t want to give that responsability to the user of our enum. You’ll note that the calls to add_item are directly in the body of the class instead of being in the initialized method. That is because initialized is only called when you create an instance of a class (x = MyClass.new) and we don’t need instances of class Color.
  2. the const_missing method is used for convenience. We want to use Color::BLUE, not Color.the_internal_hash[:BLUE]. const_missing is called automatically when you try to access a constant that doesn’t exists. We then use that hook to return the hash element corresponding to the name supplied. We take for grant that this “missing constant” is in fact the desired key for our hash.
  3. We defined an iterator “each” to iterate through the collection. We could also have created an attribute reader that returns the hash and let the user of our class iterate from there. (like that : Color.items.each do …)
  4. We don’t create instances of class Color because we don’t need to. Everything happens at the class level, or if you prefer, in the object called Color. I could have enforced that behavior by preventing (if such a thing exists in ruby) users to create instances of that class, but I don’t think it was essential.
  5. In a recent article, I said that it wasn’t useful to use an instance variable (@) inside a class method. Well, this example demonstrates that it isn’t strictly true. If your class is also the only instance you need, variables that only live in this instance are useful.

39 thoughts on “Enumerations and Ruby

  1. [code]We don’t create instances of class Color because we don’t need to.[/code]
    Actually, you have to, otherwise you’re just defining constants and not Enums.
    And
    Color.BLUE could be equal to Chess.KING
    (Enums are not just about constant values, they’re about type safety)

  2. @Ben,
    In this regard, symbols could be seen as “top level” enums. However, the advantage of putting related constants inside a class is that it adds semantics to your application. The constants can appear logically grouped instead of floating in the air on their own. Of course, I’m not saying that these custom enums should replace symbols. I just find it useful to group things that are related together.
    @Badaud,
    As you can see in my exemple, I don’t create any instance of class Color. I only work with the object Color, which is an instance of class Class. Also, you’ll note that i don’t write Color.BLUE but Color::BLUE, which triggers the const_missing method. It would be equivalent of doing : Color.const_missing(:BLUE) but there won’t be any point to access it that way! Finally, I don’t quite understand your claim about type safety. This was a practical example illustrating how to create an enum-like class in ruby. If for some reason Color::BLUE and Chess::KING would be equal… then so what? Those are integers values after all. The purpose (in this example at least) was to use an enum as a set of constants logically grouped together. (Like if “Color” was a namespace)

  3. @Rik,
    With your code, you would have to create an instance of Hash class everytime you would want to use the collection. If “Colors” is a local variable, it would goes out of scope pretty quick. If it is an instance variable, you would still have one hash for every instances of the class that contains it. What is the other option? To make your hash global? hmm.. bad idea 🙂
    If you use a class like i did and define your methods as class methods, you work with a single instance everytime : The instance of class “Class” called Color.

  4. Of course the Hash should be created in the scope where it’ll be used. That will possibly be in global scope. Why is that a bad idea? I presume you think it’s ok for your class to be in global scope?
    If you’re worried about Colors being a variable, freeze it.

  5. An enum must not be considered like a variable, but like a type.
    Plus, if you write : Colors = { :blue => 1, :red => 2, :yellow => 5 }, you’re violating a ruby coding convention which says that you must only use lowercase for naming a local / instance variable.
    So, to be nice you would have to write : colors = { :blue => 1, :red => 2, :yellow => 5 } and thus indicating clearly to the outside world that colors is a “single instance of something” instead of just “being something” on it’s own. That’s why I prefer using an enum-like class over using an instance of Hash class.

  6. 1. I agree that an enum shouldn’t be considered ‘like a variable’ – but like a type? In Ruby, an object’s type is how it quacks. If I can get the value associated with the word ‘red’ using Colors[:red], the object is of an appropriate type.
    The convention is that variables start with a lower case letter, local or not. Enumerations are (usually) treated as constant, which is why I named Colors with an initial capital.

  7. ” a ruby coding convention which says that you must only use lowercase for naming a local / instance variable”
    Actually not a convention at all but a feature of the language. In this case the name ‘Colors’ becomes a constant because it begins with a capital.
    This is an interesting use of const_missing. You’ve given me a bit to think about here.

  8. First off, you should use a Module, not a Class, since you’re not going to be instantiating objects.
    Second, I would suggest using a constant hash instead of @hash if it isn’t going to change.
    So I’d suggest:
    module Color
    COLORS = [:blue, :red:, :yellow]
    end
    or instead of defining each at all, just do Color::COLORS.each and it’s built right in.

  9. Frank, I think what @Badaud means is enums in C# (I don’t really know about enums in Java, maybe it’s the same, I only used it once a year ago, already forgot).
    In C#, if you declare a method parameter to be some kind of enum, then you have to pass EXACTLY the MEMBER of that enum. So in C#, enums is more than just some constants grouped in one place, but it also has type information. If you pass different kind of enum than the one that method expected, then the compiler will happily report it as error to you.
    But I think is quite impossible to do it in Ruby (actually, I haven’t thinking about it :D), because usually we don’t pay much attention to type information of an object. Event worse, actually Ruby has different concept about type (type-is-all-about-what-an-object-can-do vs type-is-all-about-object-class-hierarchy)

  10. Yes, you are right siroj. The enum described in this article was not an enum in the pure sense of the word… as it isn’t a “type” per se.
    Most of the time, I only need the kind of enum described in this article… or I don’t need an enum at all. I just like to “group names under a common namespace” for code readability. I rarely need something fancier.
    Thanks for your comment

  11. Consider this enum of the style you describe:
    class Status
    Complete=0
    Queued=1
    Ready=2
    Processing=3
    Waiting=4
    end
    now say I have an instance of this status:
    status=Status::Waiting
    How could I get this as a string that says “Waiting”? Even thought status is 4. Maybe create a class method in Status called to_string or something?

  12. Alex,
    That’s a good question. Instead of using a Fixnum as the value, you could use a Hash :
    class Status
    Complete={:value => 0, :string => “complete”}
    Queued={:value => 1, :string => “queued”}
    Ready={:value => 2, :string => “ready”}
    Processing={:value => 3, :string => “processing”}
    Waiting={:value => 4, :string => “waiting”}
    end
    status = Status::Waiting
    status[:value] # output : 4
    status[:string] # output : waiting
    There might be better ways to achieve what you want, but I think this solution would work fine.

  13. Hi,
    Another thing you could do (if you just need the integer values of associated symbols) is:
    Colors = [ RED = 0, YELLOW = 1, GREEN = 3]
    Then you can just loop doing: COLORS.each do { |x| do_something x }
    You can access COLORS[RED]. Of course, COLORS would be a global if you need it everywhere. But again, you can encapsulate it in a module if you wish.

  14. This design is wicked! You definitely know how to keep a reader
    amused. Between your wit and your videos, I
    was almost moved to start my own blog (well, almost…HaHa!) Fantastic job.
    I really loved what you had to say, and more than that, how you presented it.
    Too cool!

  15. It’s a shame you don’t have a donate button! I’d definitely donate to this
    excellent blog! I suppose for now i’ll settle for book-marking and adding your RSS feed to my Google account.
    I look forward to new updates and will share this blog with
    my Facebook group. Chat soon!

  16. Thank you for some other informative web site. The place else may I get
    that type of information written in such an ideal method?
    I’ve a undertaking that I am simply now running on, and I’ve been at the glance out for such info.

Leave a Reply

Your email address will not be published. Required fields are marked *