Thursday, August 12, 2010

Partially Implemented Protocols

Last time I discussed using a hash maps to improve code reuse with protocols. While we produced a set of good solutions, I couldn't help but feel like we aren't quite there. There still is a fair amount of chatter on the list about how to implement a default protocol, for example.

I started experimenting with an idea I'm calling a Partially Implemented Protocol (PIP). The idea is that you can use a PIP like you would a BaseClass in Java. After playing around for a little bit, I settled on the following solution:



The two major components is the PIP record and extend+ macro. Let me re-write yesterday's example with the new expand+ syntax.

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

(def BaseUIProtocol
 (pip UIProtocol
 {:action (fn [this evt] ...)
   :button-down (fn ([this evt]...)
   :button-up (fn ([this evt]...)})

(extend+ TerranUIType
  BaseUIProtocol
  {:render an-existing-fn})

(extend+ ProtossUIType
  BaseUIProtocol
  {:render a-different-fn})

What I really like about the use of a PIP is that it allows reference to the protocol & partial implementation with one record. You can get at the internal simply by using keyword lookup, and you can also derive a PIP from another PIP like so:

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

(extend+ ZergUIType
  (pip BaseUIProtocol up-down-override)
  {:render a-buggy-fn})

I'm also working on versions of reify+ proxy+, etc. that all work with PIPs, assuming this idea is worth exploring.

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!