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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(in-ns 'clojure.core) | |
(defrecord PIP [:protocol :impl]) | |
(defn pip? [x] (instance? clojure.core.PIP x)) | |
(defn extract-pair [p m] | |
(if (pip? p) | |
[(:protocol p) (merge (:impl p) m)] | |
[p m])) | |
(defn pip [p m] | |
(if (or (protocol? p) (pip? p)) | |
(let [[extracted-p extracted-m] (extract-pair p m)] | |
(PIP. extracted-p extracted-m)) | |
(throw (Exception. "Protocol not provided")))) | |
(defn merge-pip | |
[& pips] | |
(let [pips (map #(pip % {}) pips)] | |
(if (not (apply = (map :protocol pips))) | |
(throw (Exception. "Trying to merge different protocols.")) | |
(pip (:protocol (first pips)) (apply merge (map :impl pips)))))) | |
(defmacro extend+ [atype & proto+maps] | |
`(extend ~atype | |
~(apply concat | |
(map extract-pair | |
(partition 2 proto+maps))))) |
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.