One way to use Scala's traits is as stackable modifications. In this pattern, a trait (or class) can play one of three roles: the base, a core, or a stackable. The base trait (or abstract class) defines an abstract interface that all the cores and stackables extend, as shown in Figure 1. The core traits (or classes) implement the abstract methods defined in the base trait, and provide basic, core functionality. Each stackable overrides one or more of the abstract methods defined in the base trait, using Scala's abstract
override
modifiers, and provides some behavior and at some point invokes the super
implementation of the same method. In this manner, the stackables modify the behavior of whatever core they are mixed into.
Figure 1. Roles in the stackable trait pattern. |
This pattern is similar in structure to the decorator pattern, except it involves decoration for the purpose of class composition instead of object composition. Stackable traits decorate the core traits at compile time, similar to the way decorator objects modify core objects at run time in the decorator pattern.
As an example, consider stacking modifications to a queue of integers. (This example is adapted from chapter 12 or Programming in Scala, by Martin Odersky, Lex Spoon, and Bill Venners.) The queue will have two operations: put
, which places integers in the queue, and get
, which takes them back out. Queues are first-in, first-out, so get
should return the integers in the same order they were put in the queue.
Given a class that implements such a queue, you could define traits to perform modifications such as these:
Doubling
: double all integers that are put in the queueIncrementing
: increment all integers that are put in the queueFiltering
: filter out negative integers from a queueThese three traits represent modifications, because they modify the behavior of an underlying "core" queue class rather than defining a full queue class themselves. The three are also stackable. You can select any of the three you like, mix them into a class, and obtain a new class that has all of the modifications you chose.
An abstract IntQueue
class (the "base") is shown in Listing 1. IntQueue
has a put
method that adds new integers to the queue and a get
method that removes and returns them. A basic implementation of IntQueue
(a "core" class), which uses an ArrayBuffer
, is shown in Listing 2.
abstract class IntQueue { def get(): Int def put(x: Int) }Listing 1: Abstract class
IntQueue
.
import scala.collection.mutable.ArrayBuffer class BasicIntQueue extends IntQueue { private val buf = new ArrayBuffer[Int] def get() = buf.remove(0) def put(x: Int) { buf += x } }Listing 2: A
BasicIntQueue
implemented with an ArrayBuffer
.
Class BasicIntQueue
has a private field holding an array buffer. The get
method removes an entry from one end of the buffer, while the put
method adds elements to the other end. Here's how this implementation looks when you use it:
scala> val queue = new BasicIntQueue queue: BasicIntQueue = BasicIntQueue@24655f scala> queue.put(10) scala> queue.put(20) scala> queue.get() res9: Int = 10 scala> queue.get() res10: Int = 20
So far so good. Now take a look at using traits to modify this behavior. Listing 3 shows a trait that doubles integers as they are put in the queue. The Doubling
trait has two funny things going on. The first is that it declares a superclass, IntQueue
. This declaration means that the trait can only be mixed into a class that also extends IntQueue
.
trait Doubling extends IntQueue { abstract override def put(x: Int) { super.put(2 * x) } }Listing 3: The
Doubling
stackable modification trait.
The second funny thing is that the trait has a super
call on a method declared abstract. Such calls are illegal for normal classes, because they will certainly fail at run time. For a trait, however, such a call can actually succeed. Since super
calls in a trait are dynamically bound, the super
call in trait Doubling
will work so long as the trait is mixed in after another trait or class that gives a concrete definition to the method.
This arrangement is frequently needed with traits that implement stackable modifications. To tell the compiler you are doing this on purpose, you must mark such methods as abstract
override
. This combination of modifiers is only allowed for members of traits, not classes, and it means that the trait must be mixed into some class that has a concrete definition of the method in question.
Here's how it looks to use the trait:
scala> class MyQueue extends BasicIntQueue with Doubling defined class MyQueue scala> val queue = new MyQueue queue: MyQueue = MyQueue@91f017 scala> queue.put(10) scala> queue.get() res12: Int = 20
In the first line in this interpreter session, we define class MyQueue
, which extends BasicIntQueue
and mixes in Doubling
. We then put a 10 in the queue, but because Doubling
has been mixed in, the 10 is doubled. When we get an integer from the queue, it is a 20.
Note that MyQueue
defines no new code. It simply identifies a class and mixes in a trait. In this situation, you could supply "BasicIntQueue
with
Doubling
" directly to new
instead of defining a named class. It would look as shown in Listing 4:
scala> val queue = new BasicIntQueue with Doubling queue: BasicIntQueue with Doubling = \$anon\$1@5fa12d scala> queue.put(10) scala> queue.get() res14: Int = 20Listing 4: Mixing in a trait when instantiating with
new
.
To see how to stack modifications, we need to define the other two modification traits, Incrementing
and Filtering
. Implementations of these traits are shown in Listing 5:
trait Incrementing extends IntQueue { abstract override def put(x: Int) { super.put(x + 1) } } trait Filtering extends IntQueue { abstract override def put(x: Int) { if (x >= 0) super.put(x) } }Listing 5: Stackable modification traits
Incrementing
and Filtering
.
Given these modifications, you can now pick and choose which ones you want for a particular queue. For example, here is a queue that both filters negative numbers and adds one to all numbers that it keeps:
scala> val queue = (new BasicIntQueue | with Incrementing with Filtering) queue: BasicIntQueue with Incrementing with Filtering... scala> queue.put(-1); queue.put(0); queue.put(1) scala> queue.get() res15: Int = 1 scala> queue.get() res16: Int = 2
The order of mixins is significant. (Once a trait is mixed into a class, you can alternatively call it a mixin.) Roughly speaking, traits further to the right take effect first. When you call a method on a class with mixins, the method in the trait furthest to the right is called first. If that method calls super
, it invokes the method in the next trait to its left, and so on. In the previous example, Filtering
's put
is invoked first, so it removes integers that were negative to begin with. Incrementing
's put
is invoked second, so it adds one to those integers that remain.
If you reverse the order, first integers will be incremented, and then the integers that are still negative will be discarded:
scala> val queue = (new BasicIntQueue | with Filtering with Incrementing) queue: BasicIntQueue with Filtering with Incrementing... scala> queue.put(-1); queue.put(0); queue.put(1) scala> queue.get() res17: Int = 0 scala> queue.get() res18: Int = 1 scala> queue.get() res19: Int = 2
Overall, code written in this style gives you a great deal of flexibility. You can define sixteen different classes by mixing in these three traits in different combinations and orders. That's a lot of flexibility for a small amount of code, so you should keep your eyes open for opportunities to arrange code as stackable modifications.
Have an opinion? Readers have already posted 6 comments about this article. Why not add yours?
Bill Venners is president of Artima, Inc., which publishes the Artima Developer website at www.artima.com. He is author of the book, Inside the Java Virtual Machine, a programmer-oriented survey of the Java platform's architecture and internals. His popular columns in JavaWorld magazine covered Java internals, object-oriented design, and Jini. Bill has been active in the Jini Community since its inception. He led the Jini Community's ServiceUI project, whose ServiceUI API became the de facto standard way to associate user interfaces to Jini services. Bill also serves as an elected member of the Jini Community's initial Technical Oversight Committee (TOC), and in this role helped to define the governance process for the community.
Artima provides consulting and training services to help you make the most of Scala, reactive
and functional programming, enterprise systems, big data, and testing.