First, a disclaimer: the opinions on these posts are my own, and they reflect (for me) a design decision on the language that I don’t understand, specially considering other decisions that seems to contradict it. I also want to say that Clojure (and ClojureScript) is my favorite language, the one that I enjoy writing on my free time and professionally, so by no means this is a rant on the whole language!
Well, this is a new “series” on this blog: what is on the Clojure language that I don’t like, that I feel is out-of-place, and sometimes I can’t understand? In this first post, “keyword inheritance”. And what is that?
Clojure allows us to use derive
to generate a “parent-child” inheritance against keywords. So, for example:
(isa? ::dog ::animal) ; => false (derive ::dog ::animal) (isa? ::dog ::animal) ; => true
This will change the way multimethods work too: so, for example, if derive
is used and a multimethod expects an ::animal
and you send a ::dog
, it’ll use the implementation for ::animal
:
(defmulti cry :type) (defmethod cry ::animal [_] "Some animal crying") (cry {:type ::wolf}) ; Execution error (IllegalArgumentException) at user/eval152 (REPL:1). ; No method in multimethod 'cry' for dispatch value: :user/wolf (cry {:type ::dog}) ; => "Some animal crying"
Why it’s strange?
Clojure is a functional language. It avoids mutable data (mutability should be controlled with atom
, agent
, binding
, etc and everything is immutable by default) and global state (everything is inside a namespace, for example, so “global data” is local to the namespace). So derive
, for me, is a strange beast on this world: it is global state, and it is mutable, and affects the whole language. Worse yet, you can underive
things, making multimethod dispatch quite unpredictable if you use both. It also make inheritance even worse than it is on object oriented world: at least, inheritance depends only on the moment of creation of a class, but with derive
any keyword can inherit from any other keyword at any time! You can also conditionally derive
something, making inheritance depends on run-time conditions. Not even Ruby does this!
(Now, just kidding: it does in some places!)
Why is it bad?
First because some libraries will try to use it: for example, Integrant respects the keyword inheritance; but the worst part is that it spreads rules about how a program works in multiple places of the code, so even (require ...)
can change behavior of existing code, code that doesn’t even know that the required namespace even exist!
But I think the worst part is that it is unnecessary. Instead of doing something like the multimethod dispatch example on the beginning of the post, you can make the dependency explicit:
(def inheritance {::dog ::animal}) (defmulti cry #(let [t (:type %)] (inheritance t t))) (defmethod cry ::animal [_] "Some animal crying") (cry {:type ::dog}) ; => "Some animal crying"
The same behavior, but now is local, and immutable. And, even then, I still think there are better ways of solving this kind of code.
What about def
?
Yes, def
is somehow a “global state”. And yes, you can “mutate” the vars in a namespace by issuing multiple def
in a single namespace like:
(def a 10) (defn inc-a [] (inc a)) (inc-a) ; => 11 (def a 20) (inc-a) ; => 21
But the behavior above is not stable: it can cause problems if you AOT (compile, create an uberjar, and so on) your code, and also, I don’t know anyone that uses this kind of “feature”. One can also say that def
mutates the current namespace, and it is almost true: in fact, when the Clojure reader finds a def
anywhere in a namespace, it will generate an clojure.lang.Var$Unbound
– an Unbound Variable – and it’ll wait for the code to define its value. So, if you use (def a 10)
, you’re already defining the value for the var a
. It is almost the same behavior for declare
, in fact.
But by definition, if you have two namespaces – parent
and child
– and you require
child from parent, there’s little you can do from child
that will affect the parent
namespace: specially because the child have no way of referencing the parent, as Clojure doesn’t allow cyclic dependencies.
Now, the same is not true for derive
– if the parent have some multimethod, the child can easily derive
a symbol and change the behavior, and the parent have no way of knowing it! And, if imperative programming taught us anything, is that mutating things that influence changes in behavior is a really bad idea.