Here's example usage showing how they stack on each other:
val numToString = DynDispatch.defMulti { i: Int => i match { case 1 => "one" case 2 => "two" case 3 => "three" case _ => "bignumber" } } numToString(2) // "two" // add logging? numToString.onMethod { (i, s) => println("called numToString(" + i + ") and returned: " + s) } numToString(3) // called numToString(3) and returned: three numToString.beforeMethod { _-1 } // evil... subtract one from input numToString(3) // called numToString(3) and returned: two numToString.afterMethod { _ toUpperCase } numToString(2) // called numToString(2) and returned: ONE numToString.afterMethod { _.substring(1) } numToString(2) // called numToString(2) and returned: NE numToString.aroundMethod { f => { i: Int => f(i+1).reverse } } numToString(2) // called numToString(2) and returned: OW numToString.aroundMethod { f => { i: Int => "hello" } } numToString(2) // called numToString(2) and returned: hello
Our dispatcher definition looks like this:
object DynDispatch { class DynMethod[A,R] { import scala.collection.mutable.ListBuffer private[this] val methods = ListBuffer[PartialFunction[A,R]]() private[this] val around = ListBuffer[(A=>R) => (A=>R)]() private[this] val on = ListBuffer[(A,R)=> Unit]() def defMethod(m: PartialFunction[A,R]) = methods += m def beforeMethod(b: A => A) = around += { f => { x => f(b(x)) } } def afterMethod(a: R => R) = around += { f => { x => a(f(x)) } } def aroundMethod(a: (A=>R) => (A=>R)) = around += a def onMethod(o: (A,R) => Unit) = on += o def apply(args: A): R = { def applyChain[X,Y] = { ( x: X, f: X => Y) => f(x) } def innerApply: A => R = { a: A => methods.reverse.find(_.isDefinedAt(a)) match { case Some(f) => f(a) case None => throw new Exception("Method not defined for: " + a) } } val result = (around.foldLeft(innerApply)(applyChain))(args) on foreach { _(args: A, result: R) } result } } def defMulti[A,R] = new DynMethod[A,R] def defMulti[A,R](f: A => R) = { val dm = new DynMethod[A,R] dm.defMethod { case a => f(a) } dm } }
And, here is an example class hierarchy showing some usage:
trait Comedian { def perform(props: Props) = "Generic performance" } // comedic props trait Props case class NoProps extends Props case class SledgeHammer extends Props case class MultipleProps extends Props trait DynamicComedian extends Comedian { // make perform dynamic val perform = DynDispatch.defMulti(super.perform _) override def perform(props: Props) = perform.apply(props) } class Gallagher extends DynamicComedian { perform.defMethod { case p: MultipleProps => "Lots of stuff" } perform.defMethod { case p: SledgeHammer => "Somebody is getting messy" } } class ChrisRock extends DynamicComedian { perform.defMethod { case p: Props => "Tell African American jokes" } } class GeorgeLopez extends ChrisRock { perform.afterMethod { result => result.replaceAll("African American", "Latino") } } class GeorgeCarlin extends DynamicComedian { perform.defMethod { case p: Props => "I don't need any fuckin' props... shit!" } perform.defMethod { case p: NoProps => "Let me tell you about swear words" } } trait LoggingComedian { self: DynamicComedian => perform.onMethod { (props, result) => println("result: " + result) } } trait UnpaidComedian { self: DynamicComedian => perform.aroundMethod { perf => (props) => "Whatever, I'm not performing" } } object App extends Application { val gl = new GeorgeLopez with LoggingComedian gl.perform(NoProps()) // result: Tell Latino jokes val gc = new GeorgeCarlin with LoggingComedian gc.perform(NoProps()) // result: Let me tell you about swear words val g = new Gallagher with LoggingComedian g.perform(SledgeHammer()) // result: Somebody is getting messy val gu = new Gallagher with LoggingComedian with UnpaidComedian gu.perform(SledgeHammer()) // result: Whatever, I'm not performing }
Now, if you haven't fallen asleep by now, you are probably wondering why we would go through this trouble? The Comedian class hierarchy with mixin traits could be more clearly and performantly defined statically using standard method overrides.
For example, the LoggingComedian trait could be defined like:
trait Comedian { def perform(props: Props) = "Generic performance" } // comedic props trait Props case class NoProps extends Props case class SledgeHammer extends Props case class MultipleProps extends Props class Gallagher extends Comedian { def perform(p: MultipleProps) = { "Lots of stuff" } def perform(p: SledgeHammer) = { "Somebody is getting messy" } } class ChrisRock extends Comedian { override def perform(p: Props) = { "Tell black jokes" } } class GeorgeLopez extends ChrisRock { override def perform(p: Props) = { super.perform(p).replaceAll("African American", "Latino") } } class GeorgeCarlin extends Comedian { override def perform(props: Props) = { props match { case p: NoProps => "Let me tell you about swear words" case _ => "I don't need any fuckin' props... shit!" } } } trait LoggingComedian extends Comedian { override def perform(props: Props) = { val result = super.perform(props) println("result: " + result) result } } trait UnpaidComedian extends Comedian { override def perform(props: Props) = { "Whatever, I'm not performing" } }
So, unless you need the runtime argument specialization that defMulti provides, there is no need for the interceptors, right?
Let's run it:
object App2 extends Application { val gl = new GeorgeLopez with LoggingComedian gl.perform(NoProps()) // result: Tell Latino jokes val gc = new GeorgeCarlin with LoggingComedian gc.perform(NoProps()) // result: Let me tell you about swear words val g = new Gallagher with LoggingComedian g.perform(SledgeHammer()) // no output!! val gu = new Gallagher with LoggingComedian with UnpaidComedian gu.perform(SledgeHammer()) // no output!! }
What happened? Why is Gallagher not logging? Well, if you want to override a method in a trait, you need to override each combination of the arguments (here, SledgeHammer in addition to the generic Props).
Another useful property of the dynamic interceptors for this usage pattern is that you can use a class in the self type of the mixin, and not just a trait (what if Comedian was concrete?).
But, the most dramatic effect of these dynamic interceptors is that you can now get non-lexical, dynamic scoping. You can attach interceptors to individual instances of Comedian and send them down the call stack.
val georgeCarlin = comedianService.get("George Carlin") georgeCarlin.perform.aroundMethod { f => (props) => { myCallBack(props) "Hear the one about the observer pattern?\n" + f(props) } } myOtherService(georgeCarlin)
Any other uses?