Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

why are `disj` and `dissoc` distinct functions in Clojure?

Tags:

clojure

So far as I've seen, Clojure's core functions almost always work for different types of collection, e.g. conj, first, rest, etc. I'm a little puzzled why disj and dissoc are different though; they have the exact same signature:

(dissoc map) (dissoc map key) (dissoc map key & ks)
(disj set) (disj set key) (disj set key & ks)

and fairly similar semantics. Why aren't these both covered by the same function? The only argument I can see in favor of this is that maps have both (assoc map key val) and (conj map [key val]) to add entries, while sets only support (conj set k).

I can write a one-line function to handle this situation, but Clojure is so elegant so much of the time that it's really jarring to me whenever it isn't :)

like image 751
Daniel Shapero Avatar asked Mar 04 '15 00:03

Daniel Shapero


2 Answers

Just to provide a counterpoise to Arthur's answer: conj is defined even earlier (the name conj appears on line 82 of core.clj vs.1443 for disj and 1429 for dissoc) and yet works on all Clojure collection types. :-) Clearly it doesn't use protocols – instead it uses a regular Java interface, as do most Clojure functions (in fact I believe that currently the only piece of "core" functionality in Clojure that uses protocols is reduce / reduce-kv).

I'd conjecture that it's due to an aesthetic choice, and indeed probably related to the way in which maps support conj – were they to support disj, one might expect it to take the same arguments that could be passed to conj, which would be problematic:

;; hypothetical disj on map
(disj {:foo 1
       [:foo 1] 2 
       {:foo 1 [:foo 1] 2} 3}
       }
      {:foo 1 [:foo 1] 2} ;; [:foo 1] similarly problematic
      )

Should that return {}, {:foo 1 [:foo 1] 2} or {{:foo 1 [:foo 1] 2} 3}? conj happily accepts [:foo 1] or {:foo 1 [:foo 1] 2} as things to conj on to a map. (conj with two map arguments means merge; indeed merge is implemented in terms of conj, adding special handling of nil).

So perhaps it makes sense to have dissoc for maps so that it's clear that it removes a key and not "something that could be conj'd".

Now, theoretically dissoc could be made to work on sets, but then perhaps one might expect them to also support assoc, which arguably wouldn't really make sense. It might be worth pointing out that vectors do support assoc and not dissoc, so these don't always go together; there's certainly some aesthetic tension here.

like image 95
Michał Marczyk Avatar answered Oct 14 '22 07:10

Michał Marczyk


It's always dubious to try to answer for the motivations of others, though I strongly suspect this is a bootstrapping issue in core.clj. both of these functions are defined fairly early in core.clj and are nearly identical except that they each take exactly one type and call a method on it directly.

(. clojure.lang.RT (dissoc map key))

and

(. set (disjoin key))

both of these functions are defined before protocals are defined in core.clj so they can't use a protocol to dispatch between them based on type. Both of these where also defined in the language specification before protocols existed. They are also both called often enough that there would be a strong incentive to make them as fast as possible.

like image 42
Arthur Ulfeldt Avatar answered Oct 14 '22 07:10

Arthur Ulfeldt