Mixins Simplify Composition in Scala
Join the DZone community and get the full member experience.
Join For Freetrait MyIter [A] { def next(): A def hasNext: Boolean }
The content of this article was originally posted in Neil Carrier's blog, Techneilogy
In the first of these roles, traits can be used just like traditional interfaces: as a way of specifying the external behavior of an object without saying anything about its implementation. In the second of these roles, traits can be used like abstract base classes, supplying both declarations and definitions.
However – and this is the really cool part – since traits are not classes, multiple traits can be specified for a single object without violating single inheritance constraints. So, like interfaces, multiple traits can contribute to a class declaration, but unlike either interfaces or single base classes, multiple traits can also contribute to a class definition. This is sometimes called “mixin” inheritance, because the capabilities are “mixed into” other types.
The example here develops a simple set of iterator traits and classes. Iterators were chosen because they are a simple construct which can nevertheless illustrate some powerful mixin concepts. Of course, the standard Scala library contains a much more sophisticated set of iterator traits and classes, and you’ll want to use those rather than mine for real-world programming. In particular, to keep things simple, I ignore covariance and contravariance, which can be explicitly controlled in Scala types.
(And as always, the code and information here are presented "as-is" and without warranty or implied fitness of any kind; use it at your own risk.)
(The code is presented in fragments; a complete listing is at the bottom.)
Recalling the simple iterator example from above:
trait MyIter [A] { def next(): A def hasNext: Boolean }
This trait is parameterized on the iterated type, the “[]” brackets delineating type parameters in Scala. Also, the methods follow the standard Scala practice for methods without arguments: if the method modifies the object, parentheses are used, if it does not modify the object, the parentheses are omitted.
In this case, the iterator supplies only declarations, and so is used in an interface role. (In fact, it resembles the actual Iterator trait from Scala.) We could supply functionality via another trait or concrete class, but we’ll actually do it using an abstract class. The abstract class below defines iteration across a range of integers, and delegates the conversion from integer to iterated type to its derived classes using the abstract function “current.” Inheritors can get basic iteration capability simply by defining this function.
abstract class MyCursorIter[A] ( protected val first: Int, protected val last: Int) extends MyIter[A] { protected var cursor = first - 1 protected def current: A def next() = { if (hasNext) { cursor += 1 current } else throw new Exception("Iteration past end.") } def hasNext = cursor < last }
Note that first trait or the base class is mixed in using the “extends” keyword; subsequent traits are mixed in using the “with” keyword.
Once you have an iterator, you can use it to do all sorts of wonderful things. But perhaps the most wonderful of all is the higher order function: “fold.” Fold is a good candidate for the second role of traits: defining methods and values. Below is a definition of a fold trait as applied to the “MyIter” trait:
trait MyFold[A] { this: MyIter[A] => @tailrec final def fold[B] (acc: B, f:(B,A) => B): B = { if (hasNext) { fold(f(acc,next()),f) } else acc } }
The curious-looking line “this: MyIter[A] =>“ tells the compiler that this trait will only be applied to types that also support the “MyIter[A]” trait. This is what allows you to use members “hasNext” and “next()” from that trait. “@tailrec” on the fold function tells the Scala compiler that the function should be made tail-recursive, and to warn you at compile time if that is not possible. What follows is a standard fold function that takes an initial result “acc” (for accumulator), and steps through the elements passing on the result of applying the function “f” to the previous result and each element, finally returning the last result.
Fold is like the Swiss Army Knife of higher order functions in that it can be used to implement a whole range of other higher order functions. The fragment below shows a couple of other mixins which use fold. The first computes the length of an iterated sequence and the second converts an iterated sequence to a list. Note the use of the “this: … =>” construct to inform the compiler that these traits extend types with the fold trait. And note also the use of the “_” in “this: MyFold[_] =>” in MyLength which indicates that the type parameter is not important for this definition. Also note that both functions consume the iterator, leaving it empty.
trait MyLength { this: MyFold[_] => def length() = { fold(0, (b: Int, _) => b + 1) } } trait MyToList[A] { this: MyFold[A] => def toList() = fold(Nil, (b: List[A], a: A) => a::b).reverse }
And, for convenience, here is a trait that mixes a reset function into the “MyCursorIter” class. If an iterator can be made resettable, functions can be written that consume it without rendering it useless for further computation.
trait MyReset { this: MyCursorIter[_] => def reset() = cursor = first - 1 }
At last, time for the first concrete implementation. And here it is, “MyStringIter,” a character iterator across strings:
class MyStringIter (s: String) extends MyCursorIter[Char](0,s.length - 1) with MyReset with MyFold[Char] with MyLength with MyToList[Char] { protected def current = s(cursor) }
And the test:
object Test00 { def main(args: Array[String]): Unit = { // String example. val msi = new MyStringIter("This is a test.") println(msi.toList()) msi.reset() println(msi.length()) println("Your breakpoint here.") } }
But wait! There’s more! In Scala you can use all these traits and classes to cobble together types on the fly. The extended test below illustrates this by defining, with the test function itself, an ad-hoc iterator across integer ranges.
object Test00 { def main(args: Array[String]): Unit = { // String example. val msi = new MyStringIter("This is a test.") println(msi.toList()) msi.reset() println(msi.length()) // Range example. val msr = new MyCursorIter[Int](0,9) with MyReset with MyFold[Int] with MyToList[Int] { protected def current = cursor } println(msr.toList()) msr.reset() // Error! MyLength is not mixed-in to msr. // println(msr.length()) println("Your breakpoint here.") } }
Now isn’t that cool?
-Neil
Source: http://techneilogy.blogspot.com/
Opinions expressed by DZone contributors are their own.
Comments