Sunday, October 4, 2009

DCI using Scala traits



This blog post is inspired two articles that I saw on http://www.artima.com.




The first one is The DCI Architecture: A New Vision of Object-Oriented Programming by Trygve Reenskaug and James O. Coplien.


This article talks, among others, about roles and the way traits can be used to model them.




The second one is Scala's Stackable Trait Pattern by Bill Venners.


This article talks about how to stack Scala's trait's to add multiple roles to objects.




One thing I want to show in this blog post is how to model traits that depend on other traits using self types and how to resolve those dependencies.




Trait stacking and trait dependency are patterns that can naturally be combined.




First I introduce a trait that wraps functions and a trait that wraps predicates.


One thing you can do with functions is compose them with functions. One thing that you can do with predicates is filter procedures with them.


One reason why I define separate taits is because I want to make their correspondence with roles explicit. Another reason is that Scala's type inferencer complains when stacking more than one instantiation of the same generic trait.






trait Function[X, Y] {
def fun: X => Y

def compose[Z](fyz: Function[Y, Z]) = new Function[X, Z] {
def fun = fyz.fun compose Function.this.fun
}
}

trait Predicate[X] {
def pred: X => Boolean

def filter(px: Function[X, Unit]) = new Function[X, Unit] {
def fun = x => if(pred(x)) px.fun(x) else ()
}
}






Next I introduce a trait that wraps function transformers and a trait that wraps procedure transformers. Think of transformers as adding roles (or aspects, if you want) to functions and procedures.






trait FunTransformer[X, Y] {
def tFun: Function[X, Y] => Function[X, Y]
}

trait ProcTransformer[X] {
def tProc: Function[X, Unit] => Function[X, Unit]
}






Here are two examples: composing and filtering. Note that both examples use self types. In a way, they depend on other traits.






trait Compose[X, Y]
extends FunTransformer[X, Y] { this: Function[X, X] =>
override def tFun: Function[X, Y] => Function[X, Y] = this compose _
}

trait Filter[X]
extends ProcTransformer[X] { this: Predicate[X] =>
override def tProc: Function[X, Unit] => Function[X, Unit] = this filter _
}






Next I define two simple roles.






trait Doubling extends Function[Int, Int] {
def fun = 2 * _
}

trait IsEvenTest extends Predicate[Int] {
def pred = _ % 2 == 0
}






Next I define two complex roles. Note that, in a way, dependencies are resolved (or injected, if you want).






trait ArgumentDoubling[Y]
extends Compose[Int, Y]
with Doubling

trait ArgumentIsEvenFiltering
extends Filter[Int]
with IsEvenTest






Let's apply all this to queues. Note that AbstractQueue's abstract part is defined in terms of function values, while its concrete part is defined in terms of methods.






trait AbstractQueue[X] {
def abstractGet: Function[Unit, X]
def abstractPut: Function[X, Unit]

def get() = abstractGet.fun(())
def put(x: X) = abstractPut.fun(x)
}

import scala.collection.mutable.ArrayBuffer

class ConcreteQueue[X] extends AbstractQueue[X] {
private val buffer = new ArrayBuffer[X]
def abstractGet = new Function[Unit, X] {
def fun = _ => {
if(buffer.isEmpty) {
throw new Exception("empty queue")
} else {
buffer.remove(0)
}
}
}
def abstractPut = new Function[X, Unit] {
def fun = x => { buffer += x }
}
}






Next I define two general roles that can be added to the put method. Note that, again, they use a self type. In a way, they depend on another role.






trait PutFunModifier[X]
extends AbstractQueue[X] { this: FunTransformer[X, Unit] =>
abstract override def abstractPut = tFun(super.abstractPut)
}

trait PutProcModifier[X]
extends AbstractQueue[X] { this: ProcTransformer[X] =>
abstract override def abstractPut = tProc(super.abstractPut)
}






Here are two specific roles. Note that, in a way, dependencies are resolved (or injected, if you want).






trait PutArgumentDoubling
extends PutFunModifier[Int]
with ArgumentDoubling[Unit]

trait PutArgumentIsEvenFiltering
extends PutProcModifier[Int]
with ArgumentIsEvenFiltering






Here is a test.






object DCI {
def main(args: Array[String]) {
println("doubling")
val queue1 =
new ConcreteQueue[Int]
with PutArgumentDoubling
println("put("+5+")") ; queue1.put(5)
println("put("+10+")") ;queue1.put(10)
try {
println(queue1.get())
println(queue1.get())
} catch {
case e => println(e.getMessage)
}
println()
println("isEven filtering")
val queue2 =
new ConcreteQueue[Int]
with PutArgumentIsEvenFiltering
println("put("+5+")") ; queue2.put(5)
println("put("+10+")") ;queue2.put(10)
try {
println(queue2.get())
println(queue2.get())
} catch {
case e => println(e.getMessage)
}
println()
println("isEven filtering AFTER doubling")
val queue3 =
new ConcreteQueue[Int]
with PutArgumentIsEvenFiltering
with PutArgumentDoubling
println("put("+5+")") ; queue3.put(5)
println("put("+10+")") ;queue3.put(10)
try {
println(queue3.get())
println(queue3.get())
} catch {
case e => println(e.getMessage)
}
println()
println("isEven filtering BEFORE doubling")
val queue4 =
new ConcreteQueue[Int]
with PutArgumentDoubling
with PutArgumentIsEvenFiltering
println("put("+5+")") ; queue4.put(5)
println("put("+10+")") ;queue4.put(10)
try {
println(queue4.get())
println(queue4.get())
} catch {
case e => println(e.getMessage)
}
println()
}
}






Running the test results in.






doubling
put(5)
put(10)
10
20

isEven filtering
put(5)
put(10)
10
empty queue

isEven filtering AFTER doubling
put(5)
put(10)
10
20

isEven filtering BEFORE doubling
put(5)
put(10)
20
empty queue