Scalazine

Scala's Stackable Trait Pattern

by Bill Venners

September 30, 2009




Summary This article describes a Scala design pattern in which traits provide stackable modifications to underlying core classes or traits. This article describes a Scala design pattern in which traits provide stackable modifications to underlying core classes or traits.

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 queue

: double all integers that are put in the queue Incrementing : increment all integers that are put in the queue

: increment all integers that are put in the queue Filtering : filter out negative integers from a queue

These 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) }

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 } }

BasicIntQueue

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

Doubling

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 = 20

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

Incrementing

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.

Share your opinion

Have a question or opinion about the Stackable Trait pattern? Discuss this article in the Articles Forum topic, Scala's Stackable Trait Pattern.

About the author

Bill Venners is president of Artima, Inc., publisher of Artima Developer (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. Active in the Jini Community since its inception, Bill led the Jini Community's ServiceUI project, whose ServiceUI API became the de facto standard way to associate user interfaces to Jini services. Bill is also the lead developer and designer of ScalaTest, an open source testing tool for Scala and Java developers, and coauthor with Martin Odersky and Lex Spoon of the book, Programming in Scala.