In my previous post, I talked about the Embedded Document pattern, and about how a combination of combinators and a little metaprogramming can abstract away its recurring bits. When I wrote that post, I wondered how I might deal with the same problem in Clojure, a language that lacks OO in the traditional sense. I fiddled with it a bit and ended up with what I find to be a reasonably elegant solution. This post talks about that.
In Clojure, we like to keep our data structures naked. We do not wrap them in classes unless there is a good reason to do so. This has the huge advantage that all the functions and utilities available for maps, sets, and sequences still work with your data.
Our initial approach to the problem might look like this:
(def data {:id "product1234" :name "Nex-5R" :href "totoro.com" :links [{:id "link1" :url "kokoro.com"} {:id "link2" :url "boboro.com"}]})
(defn product-master-id [product] (:name product))
(defn link-master-id [link] (:id link))
;; Collect product master ID and master IDs for all its links
{:product-master-id (product-master-id data) :links-master-ids (map link-master-id (:links data))}Pretty simple. Just data and functions.
However, there is a disadvantage with this approach: the caller always has to know the type of the input data, whether it is a link, a product, and so on. In a system involving complex data comprising dozens of models, this would become very tedious. It is not ideal.
Wouldn’t it be nice if we could have our data respond to function calls polymorphically, without having to wrap it in some type or otherwise pollute the data? The good news is that there are a couple of Clojure features that make this possible:
- Metadata: most Clojure data structures implement the
clojure.lang.IObjprotocol, which provides a way to decorate references with metadata without affecting the data itself. We can store “type tags” for our data in that metadata. - Multimethods: these let you dispatch on an arbitrary function. In our case, we could use a dispatch function that looks up the type tag embedded in the metadata.
With that, our design might look like this:
(defn with-type-tag [data type-tag] (with-meta data {:type-tag type-tag}))
(defn type-tag [data] (:type-tag (meta data)))
(defn tag-as [tag] (fn [data] (with-type-tag data tag)))
(defmulti master-id type-tag)
(defmethod master-id ::Product [this] (:name this))
(defmethod master-id ::Link [this] (:id this))
(defmulti links type-tag)
(defmethod links ::Product [this] (->> this :links (map (tag-as ::Link))))
;; Collect product master ID and master IDs for all its links
{:product-master-id (master-id data) :links-master-ids (map master-id (links data))}Great, we got a custom tagging and dispatching system working with a tiny bit of code.
Since we are always dispatching on a type tag, we could spin up a small macro that avoids some of this duplication for us.
(defmacro defmessage [name' tag & forms] `(do (defmulti ~name' type-tag) (defmethod ~name' ~tag ~@forms)))
;; Here is how the relevant part of the previous snippet looks after we start;; using this macro
(defmessage master-id ::Product [this] (:name this))
(defmessage master-id ::Link [this] (:id this))
(defmessage links ::Product [this] (->> this :links (map (tag-as ::Link))))Cool. With just a few more lines, we also have a custom syntax for our new system.
What you see here is not a novel idea, but something used very routinely in Lisps. Some refer to it as a “custom type system”, though I personally am not a fan of that term, because “type” means something entirely different to me.
We have done a pretty fine job here, but there is a lot of room for improvement:
- There could be messages, that is, tag-dispatched polymorphic functions, with arity greater than one. The dispatch function should handle that.
- We could define a variation of
defmessagethat takes a function value. This would be useful for item 3. - We could define combinators for common cases, such as “sequence of
type-tag”. - We could add some sugar that lets one define messages for a model together in one place. Note that this would look somewhat like OO classes syntactically, but it would not limit extensibility, because what we are essentially writing are multimethods.
- You could extend this further to include things like validations. A macro for a model could generate a tagger that runs those validations before tagging the given data.
I will leave all of that for readers who feel like taking the idea a bit further.
If you found this post interesting, here are a few more links and resources that might strike your fancy: