Wednesday, August 11, 2010

Protocol Implementation Awesomeness

I recently read a post about Racket & Clojure, and there was a question about protocols raised. I wanted to take the time to discuss composition of implementations with Protocols. Of course, you probably know this term better as inheritance.

Personally, my main driver for multiple inhertiance is being able to re-use different partial implementations of a class. There are a lot a times I would want to mix & match implementations of this protocol, such as UI code. Consider the following protocol

(defprotocol UIProtocol
 (render [])
 (action [evt])
 (button-down [evt])
 (button-up [evt]))

Now, we can extend the protocol as such

(extend UIType
  UIProtocol
  {:render an-existing-fn
   :action (fn [this evt] ...)
   :button-down (fn ([this evt]...)
   :button-up (fn ([this evt]...)})

So far so good. However, suppose we have another UI type we want to define

(extend AnotherUIType
  UIProtocol
  {:render a-different-fn
   :action (fn [this evt] ...)
   :button-down (fn ([this evt]...)
   :button-up (fn ([this evt]...)})

The only thing that is different between the implementations is the rendering logic (similar behavior, different skin? What sort of problem domain has that?). Our implementation works, but it's repetitive.

In order to clean it up, we'll need to use a defining feature of Lisp: code is data. Remember we are building a data structure to define our type. As such, we can use every DRY trick we know. In our specific example, we take advantage of the fact that the extend macro is expecting a hash-map, and it doesn't care how it gets there. So, here's a new take on the code.

(def base-impl
  {:action (fn [this evt] ...)
   :button-down (fn ([this evt]...)
   :button-up (fn ([this evt]...)})

(extend TerranUIType
  UIProtocol
  (merge base-impl {:render an-existing-fn}))

(extend ProtossUIType
  UIProtocol
  (merge base-impl {:render a-different-fn})

BAM! I've overridden render with my specific implementation. We just simulated the abstract base class pattern. Now, let's see how flexible this really lets us be.

(def up-down-override
  :button-down button-down-2
   :button-up button-up-2})

(extend ZergUIType
  UIProtocol
  (merge base-impl up-down-override {:render a-buggy-fn});-p

What this let me do is define my own inheritance rules a la carte. No implicit order of operations, no compiler limitations, no confusing precedence rules. If you can figure out the map merging, you can do it. Period. That's why I think Clojure Protocols are so awesome!

4 comments:

  1. This is great. I was trying to find a way to reuse code with protocols (i.e. default implementation, meant to be overriden). In my case, very good performance is key. When you extend a type this way the method dispatch becomes dynamic, I believe. 'records' should be faster, but so far you can't perform code reuse with records, or can you?

    ReplyDelete
  2. @id - Well, this may be true. Still, a little bit of macro-fu can solve this in no time. I leave this as an exercise to the reader :)

    ReplyDelete
  3. Spoiler

    ;) It's a bit rough around the edges, but it works and it's sufficiently ugly to prefer the map-merge style.

    I think, that Rich mentioned somewhere that in a homogeneous calling scenario - that is in a tight loop where basically call the method on the same data type - performance can be pretty good with the map-merge approach, too. Here some kind of cache helps things. Nevertheless, it's of course slower (and will always be) than the direct dispatch with the inline definition in the deftype/defrecord.

    So I would carefully evaluate, whether the inline style is really needed or not.

    ReplyDelete
  4. Yes, that's me! I wonder why blogger decides that my Id=id.

    My use case is a handler for NIO events in netty. The handler gets called every time there is an event (of any type) in a channel. In a high traffic situation with multiple open sockets, this handler would get called a lot. But yes, maybe I should just run some micro-benchmarks and see if the macro-fu would even make a difference.

    Nevertheless, the fact that this "problem" can be solved with a carefully crafted macro just shows the power of Lisp in general, and Clojure in particular: Rich didn't add support for default implementation? You can add this feature into the language and move on :). Now I just need to find the time to do it :(

    ReplyDelete