2011-06-27

Scala Enums

A post to the scala-users mailing list was asking about how to do enums. While Java enums are relatively lame, they allow me to do simple things such as attach properties to enumerated values, e.g.:

public enum ColumnHeader {
 
  FirstName("First Name", true),
  LastName("Last Name", true),
  Age("Age", false);
 
  private final String headerText;
  private final boolean visibleByDefault;
 
  private ColumnHeader(headerText, visibleByDefault) {
    this.headerText = headerText;
    this.visibleByDefault = visibleByDefault;
  }
 
  public String getHeaderText() {
    return headerText;
  }
 
  public boolean isVisibleByDefault() {
    return visibleByDefault;
  }
}
 
Java provides a static method values() in your enum class that returns an array of your enum values, in the order you defined them. This is nice. Now you can do things like this:
 
public enum ColumnHeader {
  //...
  public static ColumnHeader[] getVisibleByDefaultColumnHeaders() {
    final List<ColumnHeader> visibleColumnHeaders =
      new ArrayList<ColumnHeader>();
    for (ColumnHeader header: values()) {
      if (header.isVisibleByDefault()) {
        visibleColumnHeaders.add(header);
      }
    }
    return visibleColumnHeaders.toArray(
      new ColumnHeader[visibleColumnHeaders.size());
  }
}

People switching from Java to Scala may want to do something like this, and may well find themselves expecting something similar from the scala.Enumeration class. But it's pretty hard to figure out how to extend scala.Enumeration and add properties to the extended class as well. I've tried a couple times, and I haven't figured it out. Instead, I've taken to using case objects to solve the kinds of problems that I solved with enums in Java.

 
I would end up with something like: 

object ColumnHeader {
  def values = Seq(FirstName, LastName, Age)
  def valuesVisibleByDefault = values.filter(_.visibleByDefault)
}
 
sealed abstract class ColumnHeader {
  val headerText: String
  val visibleByDefault: Boolean
}
 
case object FirstName extends ColumnHeader {
  val headerText = "First Name"
  val visibleByDefault = true
}
 
case object LastName extends ColumnHeader {
  val headerText = "Last Name"
  val visibleByDefault = true
}
 
case object Age extends ColumnHeader {
  val headerText = "Age"
  val visibleByDefault = false
}

Now you can do things like this:

object Test {
  def test = {
    println("all headers = " + ColumnHeader.values)
    println("visible headers = " +
            ColumnHeader.valuesVisibleByDefault)
    val x: ColumnHeader = LastName
    println("last name header text = " + x.headerText)
    x match {
      case FirstName => println("firstname")
      case LastName => println("lastname")
      case Age => println("age")
    }
  }
}

But be careful of doing things like this in the Scala REPL! For some reason, I ended up getting a strange type error on the line defining val x. It works fine when compiled.

Some things to note:
  1. One thing that Java used to do for me, but that I now do for myself, is explicitly list out the values in a sequence. This is kind of a drag, and a little error prone, but I haven't lost much sleep over that. A more clever implementation could get around this problem.
  2. The fact that valuesVisibleByDefault is so much nicer than the Java equivalent has nothing particularly to do with enums, but more with Scala being so much nicer than Java. To me, this is a much bigger win than the compiler generating a method to list my enum values for me.
  3. ColumnHeader class is sealed, which means all the subclasses to this class live in the same file. So nobody can spoof a ColumnHeader from the outside.
  4. ColumnHeader is abstract. The concrete subtypes are the individual case objects that match this type.
  5. The fact that we have to define headerText and visibleByDefault as vals, instead of as constructor or method arguments, is a bit of a pain. But it's a small price to pay.
  6. Particularly important is something that I haven't demonstrated so far: the fact that you can have other abstract classes in your hierarchy between the top-level class ColumnHeader, and the case object leaf classes. This allows you to be much more expressive than you can in Java. As a simple, made-up example, consider the following rewrite of the above:

object ColumnHeader {
  def values = Seq(FirstName, LastName, Age)
  def valuesVisibleByDefault = values.filter(_.visibleByDefault)
}
 
sealed abstract class ColumnHeader {
  val headerText: String
  val visibleByDefault: Boolean
}
 
abstract class DefaultColumnHeader extends ColumnHeader {
  override val visibleByDefault = true
}
 
case object FirstName extends DefaultColumnHeader {
  val headerText = "First Name"
}
 
case object LastName extends DefaultColumnHeader {
  val headerText = "Last Name"
}
 
abstract class NonDefaultColumnHeader extends ColumnHeader {
  override val visibleByDefault = false
}
 
case object Age extends NonDefaultColumnHeader {
  val headerText = "Age"
}

This example is contrived, but I hope it illustrates the basic idea. Instead of being limited to every element in your enum having precisely the same type, you can invent a whole type hierarchy, including mixin traits, type paremeters, etc., and put your enum values at the leaves of the hierarchy as case objects. This added expressiveness, in my opinion, is well worth the two limitations of this strategy as compared to Java enums: that you do not get a sequence of your enumeration values for free, and that declaring an element in your enumeration is slightly more verbose.

1 comment:

Note: Only a member of this blog may post a comment.