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 String
s, 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 Card
s with other Card
s; 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.