Groovy version of Neal Ford’s JRuby “Recorder”

In Neal Ford’s JavaOne 2009 “‘Design Patterns’ for Dynamic Languages” talk1, he presented a record/playback object as an example of a decorator. He did so in JRuby, which is probably a good call since JRuby is more slide-friendly than Groovy (more on this later), but in presenting the recorder he said something in passing about Groovy having a problem because of operators and how they correspond to methods. This struck me as odd, since (as a once and future Groovy DSL hacker) I regularly work with operators. So I decided to check it out.

The reality is that there isn’t a problem with a Groovy recorder and operators. Here’s my implementation of the invokeMethod core that makes up the Groovy recorder:

  def invokeMethod(String name, args) {
    switch(name) {
      case "record": 
        args[0].delegate = this
        args[0](this)
      break
      case "playback":
        toInvoke.each { methodName, methodArgs ->
          args[0].invokeMethod(methodName, methodArgs)
        }   
      break
      default: toInvoke << [name, args]
    }   
  }

The approach here is basically the same as in Neal’s presentation: just collect calls to the object in a list, and then playback iterates over the list. One big difference is that I decided to hijack invokeMethod instead of method_missing, because that seems to communicate the intent better: when I’m hijacking method handling itself for my own purposes, that’s a case for invokeMethod. Another big difference is that I provided a DSL-ish usage, too (in 4 lines of code!).

This fundamental approach to recording/playback is a bit broken (try recording calls for a Recorder, including record and playback): it’d be better to delegate to some internal object which implements the invokeMethod logic labeled above. But for the sake of a presentation and some succinct code flash-bang, it works.

The above code enables the following kind of usage:

def r = new Recorder()
r.foo()
r.record {
  it + 1 
  bar(3)
  -it 
  +it 
  it % 5 
}

Note the three distinct usages: explicit dereference of the Recorder instance r (the only way Neal’s Recorder works), implicit calls via delegation in the record closure, and dereference of the implicit argument in the record closure (it). Take your pick.

While there isn’t actually an issue with operators, there is some complication caused by Groovy properties. To enable the usage of Groovy properties in recording (i.e. foo.bar), two more methods are needed in the Recorder class: getProperty and setProperty. Thankfully, they can get routed to their corresponding calls in the target object like so:

  def getProperty(String propertyName) {
    toInvoke << ["getProperty", [propertyName]]
  }
 
  void setProperty(String propertyName, newValue) {
    toInvoke << ["setProperty", [propertyName, newValue]]
  }

While that extra code is a bit much to explain in a presentation (you’d have to explain GroovyObject and the nature of Groovy properties), in the real world this is a pretty small price to pay for the advantages of Groovy properties2. This complication could also theoretically be removed through a bit of modification, and I’ve opened a JIRA to discuss it (GROOVY-3584).

Here’s a full example of the code and its usage:

class Recorder implements GroovyInterceptable {
 
  def toInvoke = []
 
  def getProperty(String propertyName) {
    toInvoke << ["getProperty", [propertyName]]
  }
 
  void setProperty(String propertyName, newValue) {
    toInvoke << ["setProperty", [propertyName, newValue]]
  }
 
  def invokeMethod(String name, args) {
    switch(name) {
      case "record": 
        args[0].delegate = this
        args[0](this)
      break
      case "playback":
        toInvoke.each { methodName, methodArgs ->
          args[0].invokeMethod(methodName, methodArgs)
        }   
      break
      default: toInvoke << [name, args]
    }   
  }
 
}
 
def r = new Recorder()
r.foo()
r.record {
  it + 1 
  bar(3)
  it.baz = 5 
  -it 
  +it 
  it % 5 
}
 
class RunOnMe {
  def plus(val) { println "Adding $val" }
  def negative() { println "Running unary minus" }
  def positive() { println "Running unary plus" }
  def mod(val) { println "Moduloing this mod $val" }
  def foo() { println "Ran foo" }
  def bar(val) { println "Ran bar with $val" }
  def baz = 0 
}
 
def target = new RunOnMe()
r.playback(target)
println "Resulting baz was $target.baz"

The only thing that grates on me a bit is GroovyInterceptable: I’ve never really understood why that flag interface exists3 [EDIT: See Tug Wilson's comment]. It probably simplifies some internal error handling code to GroovyObject, but is there a problem with just routing everything through invokeMethod and calling it good?

1 Slides are purportedly on Neal’s Presentations GitHub Project, but I can’t get them to open in Preview. If you manage to get them to open, the Ruby Recorder is defined on page 101.
2 APIs like XmlSlurper and hashes as pseudo-objects are possible in Groovy but not in Ruby because Groovy provides two very possibly orthogonal kinds of messages on objects.
3 The reference to GroovyInterceptable in GROOVY-3584 is mainly to keep in line with existing conventions.


EDIT: By popular demand, here’s the more precise translation of Neal’s code into Groovy:

class Recorder {
 
  def toInvoke = []
 
  def playback(onThis) {
    toInvoke.each { name, args -> onThis.invokeMethod(name, args) }
  }
 
  def methodMissing(String name, args) {
    toInvoke << [name, args]
  }
 
}

It’s pretty much identical, except no initialize block is needed to initialize the field (Groovy has field declaration), there aren’t funny & and * symbols hanging around the arguments, and no block argument is needed on methodMissing/invokeMethod since Groovy Closures are passed just like any other argument. There is also a bit of static typing required for methodMissing‘s first parameter (a bug in Groovy, IMHO).

This implementation doesn’t support property calls on the objects since it’s a translation of Ruby and Ruby doesn’t have properties. Such handling could be added using precisely the same setProperty/getProperty defined above.

Also, note that this gets rid of the GroovyInterceptable interface.

This entry was posted in Groovy, JavaOne. Bookmark the permalink. Post a comment or leave a trackback: Trackback URL.

5 Comments

  1. Posted June 17, 2009 at 11:34 AM | Permalink

    GroovyInterceptable is my fault :)

    A few years ago I threw a hissy fit when the way in which method dispatch was unilaterally changed (from calling invokeMethod on a GroovyObject whose default implementation delegated the call to the MetaClass to calling invokeObject on the MetaClass first and then calling it on the GroovyObject if that failed). This broke a bunch of my code.

    Guillaume, every the diplomat, brokered a compromise which allowed me to keep my existing code essentially unchanged by adding GroovyInterceptable. He then dried the hot angry tears from my cheeks.

    I don’t know if it’s still true, but there used to be something wacky in the extreme in the implementation of method invocation because it involved throwing and catching an exception whose message included the result of calling toString() on each parameter. This bit me when I had a parameter whose toString() method had the side effect of fetching an RSS feed from the Net, parsing it and building an HTML document from the resulting data. It took a while to find out why the code ran so slowly:)

  2. Paul King
    Posted June 18, 2009 at 3:52 AM | Permalink

    And the tease about Ruby being slide friendly?

  3. Posted June 18, 2009 at 8:21 AM | Permalink

    A reference to this:

    While that extra code is a bit much to explain in a presentation (you’d have to explain GroovyObject and the nature of Groovy properties)[...]

    In general, the Ruby version is somewhat cleaner, as is often the case.

  4. Posted June 18, 2009 at 9:32 AM | Permalink

    Here’s a version which uses closures http://rifers.org/paste/show/9320

    I think it’s a little easier on the eye and it has the advantage of working properly with Java object that do things like having isX() for Boolean properties.

  5. Posted June 18, 2009 at 12:09 PM | Permalink

    A refactored closure version which is a little less wordy http://rifers.org/paste/show/9321

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">