Once you start using Clojure protocols to capture abstractions it is natural to want to define implementations of higher level protocols in terms of the lower level protocols. But, Clojure does not allow protocols to be extended to other protocols. At the Clojure Conj in 2010 Rich Hickey mentioned an approach to this problem in which a protocol is extended to Object as a “catch all”. Then if the protocol is used with an object that satisfies protocol X dynamically extend the class of the object to protocol Y.

For example, consider a low-level protocol for a Dog:

(defprotocol Dog (bark [_]))

And a higher level protocol for an Animal:

(defprotocol Animal (speak [_]))

We would like to be able to write something like the following to define how a Dog can participate in the Animal protocol.

(adapt-protocol Dog Animal (speak [dog] (bark dog)))

I have written a module, named clojure-adapt, that provides this adapt-protocol capability.

The adapt-protocol call registers the adapter functions in a global map that is keyed by the protocols Animal and Dog. The Animal protocol is extended to the base Object class with implementation functions that consult the global adapter map and dynamically extend the Animal protocol to the classes of objects that satisfy the Dog protocol.

If we have an object that satisfies the Dog protocol, for example, a String:

(extend-protocol Dog String (bark [s] (str "arf " s)))

Then we can use the Animal functions on the Dog:

(speak "Fido") => "arf Fido"

The adapters are only used if the object does not satisfy the protocol. So if a protocol is extended to a class, then the adapters are not used on that class even if objects of the class satisfy the protocol being adapted.

For example, if the Animal protocol and the Dog protocol are extended to the Date class the adapter is not used.

(extend-protocol Dog java.util.Date (bark [d] (str "dog as of " (.getTime d)))) (extend-protocol Animal java.util.Date (speak [d] (str "animal as of " (.getTime d)))) (bark (java.util.Date. (long 100))) => "dog as of 100" (speak (java.util.Date. (long 100))) => "animal as of 100"

When the Animal function, speak, is called on a Date object the adapter is not used even though the Date satisfies the Dog protocol. This is because the Date class is already participating in the Animal protocol.

The adapt-protocol call is tricky to use during development because once an adapter is “installed” for a class subsequent calls to adapt-protocol do not affect the class. One work-around to this is to refine an adapter by using extend-protocol with a test class. Once the adapter is working properly then register it for use via adapt-protocol.