Wednesday, January 6, 2010

Dynamic Method Interceptors in Scala

In my previous post I showed a rough simulation of Clojure's multimethods implemented in Scala. What if we wanted to extend that to allow dynamic interceptors on these methods?

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?

4 comments:

Alex Boisvert said...

Cool stuff!

HRJ said...

This is very interesting.

I am still new to this and digesting the concept.

Anonymous said...

What a great resource!

jonfsiher said...

Interesting. I'd like to see a more detailed example using Carrot Top.