Why are there so many map construction functions in clojure?
ClojureClojure Problem Overview
Novice question, but I don't really understand why there are so many operations for constructing maps in clojure.
You have conj
, assoc
and merge
, but they seem to more or less do the same thing?
(assoc {:a 1 :b 2} :c 3)
(conj {:a 1 :b 2} {:c 3})
(merge {:a 1 :b 2} {:c 3})
What's really the difference and why are all these methods required when they do more or less the same thing?
Clojure Solutions
Solution 1 - Clojure
assoc
and conj
behave very differently for other data structures:
user=> (assoc [1 2 3 4] 1 5)
[1 5 3 4]
user=> (conj [1 2 3 4] 1 5)
[1 2 3 4 1 5]
If you are writing a function that can handle multiple kinds of collections, then your choice will make a big difference.
Treat merge
as a maps-only function (its similar to conj
for other collections).
My opinion:
- assoc - use when you are 'changing' existing key/value pairs
- conj - use when you are 'adding' new key/value pairs
- merge - use when you are combining two or more maps
Solution 2 - Clojure
Actually these functions behave quite differently when used with maps.
conj
:
Firstly, the (conj {:a 1 :b 2} :c 3)
example from the question text does not work at all (neither with 1.1 nor with 1.2; IllegalArgumentException
is thrown). There are just a handful of types which can be conj
ed onto maps, namely two-element vectors, clojure.lang.MapEntry
s (which are basically equivalent to two-element vectors) and maps.
Note that seq
of a map comprises a bunch of MapEntry
s. Thus you can do e.g.
(into a-map (filter a-predicate another-map))
(note that into
uses conj
-- or conj!
, when possible -- internally). Neither merge
nor assoc
allows you to do that.
merge
:
This is almost exactly equivalent to conj
, but it replaces its nil
arguments with {}
-- empty hash maps -- and thus will return a map when the first "map" in the chain happens to be nil
.
(apply conj [nil {:a 1} {:b 2}])
; => ({:b 2} {:a 1}) ; clojure.lang.PersistentList
(apply merge [nil {:a 1} {:b 2}])
; => {:a 1 :b 2} ; clojure.lang.PersistentArrayMap
Note there's nothing (except the docstring...) to stop the programmer from using merge
with other collection types. If one does that, weirdness ensues; not recommended.
assoc
:
Again, the example from the question text -- (assoc {:a 1 :b 2} {:c 3})
-- won't work; instead, it'll throw an IllegalArgumentException
. assoc
takes a map argument followed by an even number of arguments -- those in odd positions (let's say the map is at position 0) are keys, those at even positions are values. I find that I assoc
things onto maps more often than I conj
, though when I conj
, assoc
would feel cumbersome. ;-)
merge-with
:
For the sake of completeness, this is the final basic function dealing with maps. I find it extremely useful. It works as the docstring indicates; here's an example:
(merge-with + {:a 1} {:a 3} {:a 5})
; => {:a 9}
Note that if a map contains a "new" key, which hasn't occured in any of the maps to the left of it, the merging function will not be called. This is occasionally frustrating, but in 1.2 a clever reify
can provide a map with non-nil
"default values".
Solution 3 - Clojure
Since maps are such a ubiquitous data structure in Clojure, it makes sense to have multiple tools for manipulating them. The various different functions are all syntactically convenient in slightly different circumstances.
My personal take on the specific functions you mention :
- I use assoc to add a single value to a map given a key and value
- I use merge to combine two maps or add multiple new entries at once
- I don't generally use conj with maps at all as I associate it mentally with lists