Java Callbacks Ruby Style

Java (pre version 8, anyway) doesn't have proper anonymous functions1 for things like callbacks. Instead, the Java Way is to create an anonymous subclass implementing the behaviour you care about in an overridden method, then passing that instance to whatever needs it. So you see a lot of stuff like this:

addListener(new ChangeListener() {
    public void changed (InputEvent event, Actor actor) {
        do_a_thing(event, actor);
    }
});

ChangeListener is an abstract class in LibGDX implementing a bunch of callback hooks; this snippet passes addListener an anonymous subclass that overrides one of these callbacks and does what I want it to.

The Ruby Way, on the other hand, is to use blocks. This gives you the same power but requires less code. The equivalent (hypothetical) Ruby framework's interface might look something like:

addKeyDown {|event, keycode| do_a_thing(event, keycode)}

So this becomes jarring when you wish to call Java APIs from JRuby, as I did when writing a game in JRuby.

Initially, I just bit the bullet and wrote (non-anonymous) subclasses to hold the callbacks. Soon though, the number of callbacks got larger than my personal threshold (two) and I decided to try something else.

My first attempt was to create anonymous Ruby subclasses, which is fine but not really compact:

addListener(Class.new(ChangeListener) {
    def changed(event, actor)
      do_a_thing(event, actor)
    end
  }.new())

Another downside--and this is a bit more subtle--is that local variables defined in the method calling addListener are not visible to the new class. For example, this:

def listenForThing(someEvent)
    addListener(Class.new(ChangeListener) {
        def changed(event, actor)
          do_a_thing(event, actor) if someEvent == event
        end
      }.new())
end

won't work; changed can't see someEvent.

There had to be a better way--this is Ruby, after all! And there was.

Ruby provides the method define_method, which takes a block and name and turns it into a method which is then added to the class. I added a helper function to do the work:

def newListener(klass, name, &block)
  c = Class.new(klass)

  c.send(:define_method, name, &block)
  return c.new
end

and used it like this:

addListener(newListener(ChangeListener, :changed)  {|event, actor|
    do_a_thing(event, actor)
})

This is almost Ruby-like in it's compactness, but there's one subtle quirk.

The block has redefined self to be the owner of the new method (the sub-instance of ChangeListener created by newListener), so the method do_a_thing the block is trying to call should actually belong to that, not the class which called newListener in the first place.

This isn't what most people expect when they see a block in Ruby, and so could lead to all kinds of unpleasant surprises. It also optimizes for the less-common case and is kind of unwieldly to work around:

outerSelf = self
addListener(newListener(ChangeListener, :changed)  {|event, actor|
    outerSelf.do_a_thing(event, actor)
})

Fortunately, this is easy to fix. Instead of turning the block into the method body, I just define the new method as a simple one-liner that evaluates the block:

def newListener(klass, name, &block)
  c = Class.new(klass)

  c.send(:define_method, name) { | *args| block.call(*args) }
  return c.new
end

This way, the block keeps its old self value and everything works as expected:

addListener(newListener(ChangeListener, :changed)  {|event, actor|
    outerSelf.do_a_thing(event, actor)
})

And now, we have Java callbacks that are almost as easy as their Ruby equivalents.


  1. And you could argue that Java 8 doesn't have them either. 


#  
Comments by Disqus Posted 2016-03-05 22:49:09 UTC; last changed 2016-03-13 18:57:21 UTC