Clojure basics: Dealing with maps

This is the first post in a series on Clojure basics. In this first post I'm going to take a look at the map collection and how to interact with it.

So, what is a map in Clojure?

A map is an immutable collection of key and value pairs.

Consider the following map holding information about this post.

{:title "Dealing with maps"
 :author {:name "Marius"
          :twitter "mariushe"}
 :tags '("Clojure" "Programming")}

As you can see, the structure reminds a bit of traditional map structures. However, there may be a few things that stands out if you're new to Clojure.

First, where the heck are the commas? Well, in Clojure whitespace is the separator. The compiler won't stop you if you use commas, but it will be read as whitespaces.

The next thing that you probably wonder about is that the keys start with a colon. These are keywords.

Keywords are symbolic identifiers that evaluate to themselves.

;=> :aKeyword

It's worth mentioning that even if it's common to use keywords as keys, you could use any Clojure data structure.

Moving over to the values in the map, you'll see that we have three different types. A String, a nested map and a list.

Now that we have established our map and what the different elements are, let's start to investigate how we could interact with it.

To keep our examples clean, let's bind our map to a variable.

(def post {:title "Dealing with maps"
           :author {:name "Marius"
                    :twitter "mariushe"}
           :tags '("Clojure", "Programming")})

;=> #'user/post

Fetching data

One thing you'll notice pretty fast, is that there are a range of different ways to get things done.

As an example of this, let's take a look at how you can fetch some data from the map.

(get post :title)
;=> "Dealing with maps"

(post :title)
;=> "Dealing with maps"

(:title post)
;=> "Dealing with maps"

All of these approaches returns the title of the post.

In the first example, we use the get function. It takes a map and a key, returning the value associated with that key.

Now what about the other two? We don't specify any functions there, do we? Well, that is exactly what we do.

The thing is, a map is also a function. So by calling a map as a function with the key as an argument, it will return the value associated with that key.

The same goes for the third example. Keywords are also functions. It will try to find itself in the map, then return the associated value if found.

What about nested values?

Now how could we get the authors twitter account within the nested author map?

Again, we have a couple of options.

First, we could use get-in.

(get-in post [:author :twitter])
;=> "mariushe"

As you can see, we just pass our map and a sequence of keys, which shows the path down the nested maps, to the get-in function.

Another option is to use the thread-first macro.

(-> post :author :twitter)
;=> "mariushe"

This is a macro that deserve it's own post, so I won't go into detail here. But it roughly translates to (:twitter (:author post)).

What if we just want a part of the map?

Let's say that we want to remove the tags from our post.

Again, let's take a look at two approaches solving this problem.

The first approach is focusing on which parts you want to keep.

(select-keys post [:title :author])
;=> {:author {:name "Marius"
;             :twitter "mariushe"}
;    :title "Dealing with maps"}

The select-keys takes our map as an argument together with a sequence of the keys we want to keep.

The next approach focus on what you want to remove.

(dissoc post :tags)
;=> {:author {:name "Marius"
;             :twitter "mariushe"}
;    :title "Dealing with maps"}

dissoc takes our map as an argument together with the key we want to remove. It's worth mentioning that we could just list up more keys to remove if that is wanted.

Let's now confirm that these functions work as expected by using another function, contains?, to check for the presence of the tags key.

  (select-keys post [:name :author])
;=> false

  (dissoc post :tags)
;=> false

Add data to the map

Until now we have focused on fetching different data from our map. What about adding data?

As mentioned earlier, maps are immutable. This means that we're not actually changing the map, but creating new ones.

Let's start by adding an excerpt to our map.

(assoc post :excerpt "In this (...)")
;=> {:author {:name "Marius"
;             :twitter "mariushe"}
;    :title "Dealing with maps"
;    :excerpt "In this (...)"
;    :tags ("Clojure" "Programming")}

assoc takes our map as an argument together with keys and values we want to add.

But what would happen if we tried to add a key/value pair for a key that already exist?

(assoc post :title "An other title")
;=> {:author {:name "Marius"
;             :twitter "mariushe"}
;    :title "An other title"
;    :tags ("Clojure" "Programming")}

As you can see, the old title got overwritten.

So what if we want to add additional data to a key? E.g. a co-author?

This could be solved using the excellent merge-with function.

merge-with takes a function and n number of maps. It will then use the specified function to merge the maps.

Let's see how this solves our co-author problem.

 {:author {:name "Adam"
           :twitter "adammscisz"}})

;=> {:author ({:name "Marius" 
;              :twitter "mariushe"}
;             {:name "Adam"
;              :twitter "adammscisz"})
;    :title "Dealing with maps"
;    :tags ("Clojure" "Programming")}

As you can see in the example above, merge-with merges our post map with the co-author map using the list function, resulting in a map with a list of authors.

I hope you enjoyed this little post on how to deal with maps. I'll be back with more posts on Clojure basics soon.

For further reading, make sure to checkout ClojureDocs.