Alex headshot

AlBlue’s Blog

Macs, Modularity and More

Introduction to Scala – traits

2007, howto, scala

In the previous post, we discussed case classes. In this post, we'll be looking at traits and how to use them to achieve sorting.

Think of a trait as an interface with non-abstract methods. A class can implement any number of traits, and these traits can require abstract methods be added or provide concrete methods to that class. It's also possible to use a trait as a type, in much the same way that interfaces work.

Let's say that we wanted to add a trait that would generate XML data for our card examples. We might want to ensure that a class of methods have a toXML method, to supplement the toString that they all have already. We can define the trait as follows:

scala> trait XML {
     |   def toString(): String
     |   def toXML(): String = "<![CDATA[" + toString() + "]]>"
     | }
defined trait XML

We can then use this trait for anything which has a toString method (which, let's face it, is everything) and then add the toXML implementation. It's quite like having an interface that defines it as an abstract method, but if it's extended or updated in the future, the trait provides a (nearly) issue-free way in updating it. And, if the default toXML method doesn't satisfy, you can always override it in the subclass.

scala> object Cards { // see previous post
     |   sealed abstract class Suit
     |   case class Club extends Suit    { override def toString() = "Clubs"    }
     |   case class Diamond extends Suit { override def toString() = "Diamonds" }
     |   case class Heart extends Suit   { override def toString() = "Hearts"   }
     |   case class Spade extends Suit   { override def toString() = "Spades"   }
     | }
defined module Cards
scala> case class Card(value: Int, suit: Cards.Suit) extends XML {
     |   override def toString() = value + " of " + suit
     | }
defined class Card
scala> val h8 = new Card(8, Cards.Heart())
h8: Card = 8 of Hearts
scala> val h8xml = h8.toXML
h8xml: String = <![CDATA[8 of Hearts]]>

If we wanted to change the trait's default implementation to provide a different XML representation, we could implement it simply by overriding:

scala> case class Card(value: Int, suit :Cards.Suit) extends XML {
     |   override def toString() = value + " of " + suit
     |   override def toXML() = "<card suit=\"" + suit + "\" value=\"" + value + "\"/>"
     | }
defined class Card
scala> val h8 = new Card(8, Cards.Heart())
h8: Card = 8 of Hearts
scala> val h8xml = h8.toXML
h8xml: String = <card suit="Hearts" value="8"/> 

Note that we have to use override when defining the toXML, since we've inherited the one from the trait.

Scala also has the ability to represent XML elements directly in the source, so instead of using a String-based concatenater to generate the XML element, we can create an element directly:

scala> case class Card(value: Int, suit :Cards.Suit) extends XML {
     |   override def toString() = value + " of " + suit
     |   override def toXML() = <card suit={suit toString} value={value toString}/> toString
     | }

The reason we have to use toString here is because the XML element must be composed of Strings, and the face value of the card is an Int in this case. Lastly, the type of the XML element is scala.xml.Elem, which would otherwise be incompatible with the return type of String defined in the trait. (Had we known about literal XMLs initially, we might have used that instead ... ah well, you can't have everything. Some things you can fix with traits, but changing the return type of an existing trait isn't a good idea generally.)

A good example that's built-in to Scala is the Ordered trait; this allows values to be compared to each other and used in sorting routines. Sorting only needs one comparison function (to compare any two elements) but having defined that, other relations follow. For example, if you know that A is bigger than B, then you also know that B is less than A. Instead of having to implement all these methods, you only need implement one of them and the rest are provided by the Ordered trait:

scala> case class Card(value:Int,suit:Cards.Suit) extends Ordered[Card] {
     |   override def compare(that:Card):Int = this.value compare that.value
     | }
defined class Card

Although conceptually similar to Java's Comparable interface, Scala's is different in two important ways.

Firstly, Scala's use is type-safe; that is, you can only compare Cards with other Cards; if you try to compare a Card with a Suit, Scala would give you a compile-time error. (Java, on the other hand, would not give you a compile-time error but would result in a runtime error, quite possibly ClassCastException.) This is achieved because the Ordered trait is generic; and we're using Ordered[Card] which ensures that the type of the compare method takes Card and nothing else.

Secondly, the Ordered trait not only ensures we define the compare method; it also provides other derived methods for free, including >, >=, < and <= — which means that we can now use these to compare our Card instances, and store them in a TreeSet:

scala> new Card(5,Cards.Heart()) < new Card(7,Cards.Spade())
res1: Boolean = true
scala> val tree = new scala.collection.jcl.TreeSet[Card]
tree: scala.collection.jcl.TreeSet[Card] = []
scala> tree add new Card(5,Cards.Heart())
res2: Boolean = true
scala> tree add new Card(3,Cards.Spade())
res3: Boolean = true
scala> tree add new Card(7,Cards.Diamond())
res4: Boolean = true
scala> tree
tree: scala.collection.jcl.TreeSet[Card] = [Card(3,Spades), Card(5,Hearts), Card(7,Diamonds]

In this post, we looked at traits as a means of adding both abstract and non-abstract functionality to an existing class. We touched on Scala's ability to deal with literal XML elements, and how the Ordered trait can be used to add an ordering over classes.