Sunday, October 16, 2011

Overriding Protocol Methods with ns-utils/immigrate

Recently I found myself experimenting with Lau Jensen's ClojureQL. Based on relational algebra, all queries to the database are expressed as relations. The user of the library applies functions on these relations to transform them via limit, join, order etc.

Internally, these relations are implemented in a record called RTable, which implements the Relation protocol. Of course the RTable record type is inaccessible to users of the library, they simply call the table function which returns a simple relation on the requested SQL table.

The reason I wanted to discuss ClojureQL's records is that they were preventing me from enhancing the RTable's implementation of the Relation protocol's methods -- or so I thought.

From my limited understand, protocol methods implemented in a record are dynamically compiled into a corresponding class which implements the protocol. This is what allows them to use primitives in the arguments. However - this is what I didn't realize - these methods are still accessible as vars in the parent namespace and thus can be referenced from elsewhere.

Using ns-utils/immigrate, references to all vars in a specified namespace are added to the current namespace. The effect is sort of like the use function, but with the immigrated functions appearing to other name spaces as though they were defined there.

This may be more clear with an example. The following is the external namespace containing the protocol and its implementation.


(ns override.external)

(defprotocol FooProtocol
(foo [this])
(bar [this]))

(defrecord MyRecord [val] FooProtocol
(foo [this] (str "Foo of " val))
(bar [this] (str "Bar of " val)))

(defn make-record [val]
(MyRecord. val))


Now say that you want to wrap any calls to foo with your own custom wrapper defined in a test namespace. Just immigrate the external namespace and implement a new foo function with a signature which matches that of the original protocol method.


(ns override.test
(:require override.external)
(:use [clojure.contrib.ns-utils :only [immigrate]]))

(immigrate 'override.external)

(defn foo [rec]
(str "Wrapping foo (" (override.external/foo rec) ")"))


Now anyone who references the override.test namespace will get all the functionality of the external namespace but with the custom behavior of the test namespace added for foo.


override.test> (def r (make-record "cucumber"))
#'override.test/r
override.test> (foo r)
"Wrapping foo (Foo of cucumber)"
override.test> (bar r)
"Bar of cucumber"
override.test>


I am unsure of the performance cost of the indirection necessary to call a namespace function followed by a compiled method, so users concerned with performance should tread carefully.

Finally, the beauty of protocols is that this should work for any type implementing FooProtocol, not just MyRecord.

No comments:

Post a Comment