Sunday, June 15, 2008

Scala and Enums

Scala doesn't have language level support for enumerations, but I think its fairly easy to argue that its a good thing. First, something isn't quite right about Java enums. Sometime soon I'll post more about that. Scala is such a nice language that you can do things cleanly without needing built in support for extra things like enum. Extra features in a language clutter it up.

As an example, I lovingly ripped off the Planets example from the Java tutorial itself, and implemented it in Scala. Here is the Scala code.



case object MERCURY extends Planet(3.303e+23, 2.4397e6)
case object VENUS extends Planet(4.869e+24, 6.0518e6)
case object EARTH extends Planet(5.976e+24, 6.37814e6)
case object MARS extends Planet(6.421e+23, 3.3972e6)
case object JUPITER extends Planet(1.9e+27, 7.1492e7)
case object SATURN extends Planet(5.688e+26, 6.0268e7)
case object URANUS extends Planet(8.686e+25, 2.5559e7)
case object NEPTUNE extends Planet(1.024e+26, 2.4746e7)
case object PLUTO extends Planet(1.27e+22, 1.137e6)

// mass in kilograms, radius in meters
sealed case class Planet( mass: double, radius: double ){
// universal gravitational constant (m3 kg-1 s-2)
val G = 6.67300E-11
def surfaceGravity = G * mass / (radius * radius)
def surfaceWeight(otherMass: double) = otherMass * surfaceGravity
}


And here is the original Java code.


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();
}
}




The Scala code is nicer, though its unfortunate that you have to say "extends Planet" for each Planet. Each planet is defined as a Scala "object" which is really nothing more than a singleton, which is what enum values are in Java.

I could and should go into more detail on all of this, but I'm mostly posting it for my own reference.

8 comments:

  1. What are the reasons for not using scala.Enumeration? I find it odd that it was not mentioned at all.

    ReplyDelete
  2. Still you cannot enumerate on this values programaticaly. Java has static method values which allows this. You can use Scala's enumeration but it also has issues. You need to use value method to initialize every member, but it only has parameters for id and name. I also tried to use objects for this but also had some issues.

    ReplyDelete
  3. As jau said, scala.Enumeration is severely limited in what it can do. Values can only take ints and Strings, and so you cant really do anything nice with them. They seem ok if you need no behavior whatsoever, and just need a set of identifiers.

    The planets approach I've used here is limited as well, you can't enumerate the values. I could get this behavior by monkeying around and storing them in some map in the constructor, but I hate it. I find myself doing this all the time in Java, because the name that you get with the enum constants (because of convention) is upper case. Sometimes I want to access the value by something other than its upper case name. It's ugly, I know it, I hate it, but I don't see an easy way around it.

    I'll try to post some examples on this soon.

    ReplyDelete
  4. Allow me to explain my comment in more detail.

    I agree that scala.Enumeration is limited, but I think that it is an error to fail to mention it.

    Below is the scala.Enumeration implementation for comparison. Some problems: you have to remember to modify some of the methods every time you add a value to the enumeration, it is a bit verbose, and does not scale well (e.g. adding more methods similar to mass and radius). For many uses, though, none of those are issues. I just think it is a disservice not to mention the existence of scala.Enumeration and point out some of its shortcomings before providing an alternative. "Scala doesn't have language level support for enumerations" is an incomplete statement.

    class Planet extends Enumeration {
    val MERCURY, VENUS, EARTH, MARS,
    JUPITER, SATURN, URANUS,
    NEPTUNE, PLUTO = Value

    val G = 6.67300E-11

    def surfaceGravity = G * mass / (radius * radius)
    def surfaceWeight(otherMass: Double) = otherMass * surfaceGravity

    def mass = this match {
    case MERCURY => 3.303e+23
    case VENUS => 4.869e+24
    case EARTH => 5.976e+24
    case MARS => 6.421e+23
    case JUPITER => 1.9e+27
    case SATURN => 5.688e+26
    case URANUS => 8.686e+25
    case NEPTUNE => 1.024e+2
    case PLUTO => 1.27e+22
    }

    def radius = this match {
    case MERCURY => 2.4397e6
    case VENUS => 6.0518e6
    case EARTH => 6.37814e6
    case MARS => 3.3972e6
    case JUPITER => 7.1492e7
    case SATURN => 6.0268e7
    case URANUS => 2.5559e7
    case NEPTUNE => 2.4746e7
    case PLUTO => 1.137e6
    }
    }

    ReplyDelete
  5. Chris, how do you use your planet class ?

    (new Planet).MERCURY.radius ?

    ReplyDelete
  6. Looks like a Scala match/case on enum is going to be about 1000 times slower than a Java switch/case on Enum.

    ReplyDelete
  7. In the initial example, Planet should probably not be a case class, but instead an ordinary sealed abstract class due to potential issues with case class inheritance (such as unexpected results from equals())

    ReplyDelete
  8. Jack,

    to access enum constants not by their upper-case name but by some other name, use something like the following.

    enum Foo {

    BAR, BAZ;

    private static final Map MYNAMES = new HashMap();
    static {
    for (Foo foo : values()) {
    MYNAMES.put(foo.myName(), foo);
    }
    }

    public static Foo forMyName( String myName ) {
    return MYNAMES.get(myName);
    // TODO: throw exception if name is unknown
    }

    public String myName() {
    return ...whatever...;
    }

    }

    I find this solution rather elegant, but that's a matter of taste, of course...

    (Filling the map in the constructor would be awkward because the constructor calls for the enum constants happen before the static initializer for the map.)

    Christopher

    ReplyDelete