Stackable Trait in Scala
10 Nov 2022 by dzlab
I come across some old scala code that uses what turns out to be a very rare pattern in Scala called Stackable Trait. The only reference to this pattern I could find on the Internet was this old article. In this article, we will explore how it can be used with a toy example.
Pattern
From a high level, this pattern aims to reduce the boilerplate code needed to combine multiple implmentations but:
- letting us write those implementations in different
trait
s and - then combining their functinality by simply extenting all of those
trait
.
It can be implemented like this:
- First, declare a base trait with the functionality we want to stack
trait T {
def func(): Unit = ()
}
- Then create couple of implementation traits that does different things when
func()
will be called
trait T1 extends T {
abstract override def func(): Unit = {
super.func()
// implementation here
}
}
trait T2 extends T {
abstract override def func(): Unit = {
super.func()
// implementation here
}
}
// more implementations
- Finally, we can stack those implementation in a class like this
class T3 extends T with T1 with T2
// or class T4 extends T with T2 with T1
Now if we call func()
on an instance of T3
both implementation from T1
and T2
will be called in that order.
Note how the implementation functions uses
abstract
and that inside them we call the parent implementation withsuper.func()
. This subtle details is actually what makes the pattern works, If we omit one of those details it will not work.
Example
Let’s create a concrete example to better understand how this pattern works. In this example, the interfaces will simply add numbers to a queue so we could tell the order they were called.
First, we define the interfaces
trait T {
val queue = scala.collection.mutable.Buffer[Int]()
def inc(): Unit = ()
}
trait T1 extends T {
abstract override def inc(): Unit = {
super.inc()
queue += 1
}
}
trait T2 extends T {
abstract override def inc(): Unit = {
super.inc()
queue += 2
}
}
Now, we create instances and call our stacked function couple times to see how it is behaving.
- using the implementation order
T1
thenT2
class T3 extends T with T1 with T2
val t = new T3()
// t.queue shouldBe Seq()
t.inc()
// t.queue shouldBe Seq(1, 2)
t.inc()
// t.queue shouldBe Seq(1, 2, 1, 2)
Note how in this case the implementation of
T1
is called before the implementation ofT2
- using the implementation order
T2
thenT1
class T4 extends T with T2 with T1
val t = new T4()
// t.queue shouldBe Seq()
t.inc()
// t.queue shouldBe Seq(2, 1)
t.inc()
// t.queue shouldBe Seq(2, 1, 2, 1)
Note how in this case the implementation of
T2
is called before the implementation ofT1
.
That’s all folks
The Stackable trait is an interesting pattern and enables us to write cleaner code by omitting the need to write explicit code to combine multiple implementations. Hopefully from now on you can use it or when you come across it in a code you’re revewing you will be able to recognize it.
I hope you enjoyed this article, feel free to leave a comment or reach out on twitter @bachiirc.