Clojure is a dynamic programming language. This is a huge plus in my opinion. It really makes fast development easy. Especially when fishing in troubled waters for a rapid prototype (or when you are a lone fighter).
However dynamic behaviour usually comes at a runtime cost. Clojure
provides several means to gain speed at cost of its dynamic nature. One
example are type hints. Another – not so obvious – example are inline
definitions of protocol functions in a defrecord
. What are the
trade-offs? And what should be the default?
There are basically two different way to extend a protocol. Let's consider an (as of now hypothetical) seq protocol of Clojure's sequences.
(defprotocol Sequable
(seq [coll] "Returns a sequence view on the collection"))
(defprotocol Sequence
(first [s] "Returns the first item of the sequence")
(rest [s] "Returns the rest of the sequence, () in case there is no rest")
(next [s] "Returns the rest of the sequence, nil in case there is no rest"))
The first way to extend this protocol is to specify the corresponding
functions inline in the defrecord
definition.
(defrecord RecordSeq [rec ks]
Sequence
(first [this] ((first ks) rec))
(rest [this] (or (next this) ()))
(next [this] (when-let [rest-ks (next ks)]
(RecordSeq. rec rest-ks))))
(defrecord Record [a b c]
Sequable
(seq [this] (RecordSeq. this [:a :b :c])))
The second is to provide a map of functions, which implement the functions of a protocol for a given type.
(defrecord RecordSeq [rec ks])
(extend ISeq
RecordSeq
{:first (fn [this] ((first (:ks this)) (:rec this)))
:rest (fn [this] (or (next this) ()))
:next (fn [this] (when-let [rest-ks (next (:ks this))]
(RecordSeq. (:rec this) rest-ks)))})
So. What are actually the differences? Well, the second is much more dynamic than the first variation, but the first is faster because it directly calls the methods via the protocol interface. So what do we gain by specifying the methods in a map and not inline? Since it is slower, that can only be bad, can't it?
Well, actually no. We gain actually a lot by staying dynamic. For example we can provide default implementations for the methods.
Have a look at rest
. In this example it is (as in c.l.ASeq
) defined
by virtue of next
. However we would have to repeat its definition
every time defining a new seq type if there was no more clever way to
handle rest
. *Assuming the new seq type would be happy with the default,
of course.*
Using extend – however – we can define default implementations and merge in our specialised variants. We can even go the route of Haskell and provide several default implementations defined in terms of each other. The actual type just has to provide one of them to make things work.
(def sequence-default-methods
{:rest (fn [this] (or (next this) ()))
:next (fn [this] (seq (rest this)))})
(extend ISeq
RecordSeq
(merge sequence-default-methods
{:first (fn [this] ((first (:ks this)) (:rec this)))
:next (fn [this] (when-let [rest-ks (-> this :ks next)]
(RecordSeq. (:rec this) rest-ks)))}))
Another point we get for free using the dynamic extend
is mutually
recursive types. If we specify the functions inline we need a factory
for at least one type.
(defprotocol Flipable
(flip [thing]))
(declare new-Right)
(defrecord Left [x]
Flipable
(flip [this] (new-Right x)))
(defrecord Right [x]
Flipable
(flip [this] (Left. x)))
(defn new-Right
[x]
(Right. x))
Depending on whether you use factory functions or not this might be
annoying. With extend
there is no need for factory functions since we
can define the types separately from the implementation of the
protocols.
(defrecord Left [x])
(defrecord Right [x])
(extend-protocol Flipable
Left
(flip [this] (Right. (:x this)))
Right
(flip [this] (Left. (:x this))))
Protocols are not the only place where we have a trade-off of static vs. dynamic. There are several places where such a trade-off can be spotted.
A
, you
notice, that you want to plugin in a B
.There is a trade-off in static vs. dynamic. static is faster. But is it necessary? Most people won't be able to answer this question, because they never did the necessary research. They just assume it. Clearly one should answer this thoroughly before jumping through hoops just to gain speed, which is eaten up by a bad algorithm.
I think one shouldn't be afraid to use the dynamic features if it makes code more concise or easier to understand. If concerns like above don't apply or if the speed gain is really worth the trouble one has always the option of deploying static features.
Published by Meikel Brandmeyer on .
I'm a long-time Clojure user and the developer of several open source projects mostly involving Clojure. I try to actively contribute to the Clojure community.
My most active projects are at the moment VimClojure, Clojuresque and ClojureCheck.
Copyright © 2009-2014 All Right Reserved. Meikel Brandmeyer