Wednesday, January 6, 2016

The curious incident of the Scala enum

One of the strangest things about Scala is its concept of enumerations, typically implemented by extending Enumeration, for example:

  object Suit extends Enumeration {
     type Suit = Value
     val Spades, Hearts, Diamonds, Clubs = Value
  }

I won't go into the details of the syntax of this (I don't understand it myself!) but I will observe that this mechanism works reasonably well. The only odd thing here is that if you actually want to refer to Spades, for instance, in your code, you will need to write something like this:

  import Suit._

But you will find that the suit values have an ordering that you would expect without you having to do anything about it, as in this bit of Scalatest code for instance:

   "suits" should "be ordered properly" in {
     val suitList = List(Clubs,Hearts,Spades,Diamonds)
     suitList.sorted shouldBe List(Spades,Hearts,Diamonds,Clubs)
   }

But what if we want to know the color of a suit? How can we add a method to this enumeration (in a manner similar to the way it is done in Java?). It's tricky! -- my first (obvious) attempt did not succeed. I am indebted to Aaron Novstrup and StackOverflow for this somewhat elegant solution:

  object Suit extends Enumeration {
    type Suit = Value
    val Spades, Hearts, Diamonds, Clubs = Value
    class SuitValue(suit: Value) {
      def isRed = !isBlack
      def isBlack = suit match {
         case Clubs | Spades => true
         case _              => false
      }
    }
    implicit def value2SuitValue(suit: Value) = new SuitValue(suit)
  } 

There is a competing style of enum in Scala: based on a case object. If you're already familiar with case classes (you should be if you're reading and understanding this), then you will probably know that a case object is just like a case class but has no parameters. One big advantage of using case objects is that matching will result in warnings if we miss a case. It's also very obvious if we want to add further methods such as our color methods. Note the trait Concept. In reality, I also have defined Rank in addition to Suit, and since they have very similar implementations, they both extend Concept.

So, let's implement our suits again:

  trait Concept extends Ordered[Concept] {
    val name: String
    val priority: Int
    override def toString = name
    def compare(that: Concept) = priority-that.priority
  }
  object Concept {
    implicit def ordering[A <: Concept]: Ordering[A] = Ordering.by(_.priority)
  }
  sealed trait Suit extends Concept {
    def isRed = !isBlack
    def isBlack = this match {
      case Spades | Clubs => true
      case _ => false
    }
  }
  case object Spades extends Suit { val name = "Spades"; val priority = 3 }
  case object Hearts extends Suit { val name = "Hearts"; val priority = 2 }
  case object Diamonds extends Suit { val name = "Diamonds"; val priority = 1 }
  case object Clubs extends Suit { val name = "Clubs"; val priority = 0 }

The key to this working correctly (and allowing, for example, List(Spades,Clubs) to be sorted without any fuss) is the ordering definition in Concept. It would be possible for Concept not to extend Ordered but then we would not be able to compare two suits directly using a "<" or similar operator.
If you decide to implement this without trait Concept and simply defining its methods directly in Suit, you would find that you do not need to define any implicit ordering. However, in order to sort a list of suits, you would have to explicitly define the type of your list as for example:

  List[Suit](Clubs,Spades)

This is because Suit appears as Suit with Product etc. This is the kind of thing that makes working with implicit values so tricky.

If you'd like to see the code for all this, including unit tests, then go to my Scalaprof site on github. and drill down to edu.neu.coe.scala.enums.

No comments:

Post a Comment