Clojure from the ground up

Vectors, maps and sets

The three collections you use every day: a row of values, a table of keys, and a bag that answers yes or no when you ask.

Part 3 of 12

Beginner16 min readBack to Courses

A vector is a row

A vector is a row of values in an order you can count on, written with square brackets: [10 20 30]. Read a slot with (get v 1) or (nth v 1), and remember that positions count from zero — slot 1 is the second value. count says how many there are, first hands you the front one, and rest hands you everything after it. Reading a slot costs about the same wherever it sits, so a vector is the right shape when position is part of the meaning.

(conj v 40) adds to the end and gives you a new vector. The original is untouched, which is a habit worth noticing early: in Clojure you build a slightly different value rather than changing the one you have. The old vector and the new one share almost all of their structure, so adding the thirty-first value does not copy the first thirty.

A map is a small table

A map pairs a key with a value, written with curly brackets: {:who "Ada" :age 36}. (get m :age) reads a value, (:age m) is the same read in a shorter spelling, and (assoc m :city "London") adds an entry, again as a new map. assoc can add a key or replace one; either way the answer is a new map and the original still holds what it held before.

A key that is not there gives you nil, or a default you supply: (get m :city "unknown"). Use keywords for your keys so they read the same in the source as they do in the output.

A set is a bag of answers

A set holds each value once, written #{:lisp :clojure}, and it is built for one question: is this value in there? (contains? tags :lisp) answers true or false, (conj tags :jvm) adds one, and a value that is already in simply does not change anything. The answer costs about the same however many values the set holds, because the set is a hash table rather than a list to search.

Because a set has no order of its own, printing one can come out in any order. Sort it before you show it to a reader, and the output stays the same every time the program runs.

One name, three kinds of adding

conj is one name for "add" across the collections, but each collection decides what adding means. A vector puts the value at the end, a set keeps it only if it is new, and a map expects a pair: (conj {:a 1} [:b 2]) adds the key :b with value 2. The pair is a two-item vector because a map entry is a key and a value; passing :b and 2 as separate arguments gives conj three arguments and an error.

The shared name lets a function add to a collection without checking its type, but the shape of the value you add has to match. For a map, conj wants a two-item vector, not two separate arguments, because a map entry is itself a pair.

Collections can be called

A map is a function from keys to values, so (who :age) is the same read as (:age who). A keyword is a function too, which is why (:age who) works and why (map :age people) will read one field from every row.

A set is a function as well: (guests "ada") answers "ada" when it is present and nil when it is not, which makes a set a natural test in filter. Vectors are callable too, with a position: (nums 1) reads slot 1, though get spells the intent more plainly.

Three collections, the same shape of call

(def nums [10 20 30])
(println nums)
(println (count nums))
(println (get nums 1))
(println (nth nums 1))
(println (first nums))
(println (rest nums))
(println (conj nums 40))

(def who {:who "Ada" :age 36})
(println (:who who))
(println (get who :age))
(println (get who :city "no city"))
(println (:city (assoc who :city "London")))

(def tags (set [:lisp :clojure :lisp]))
(println (count tags))
(println (contains? tags :lisp))
(println (sort (conj tags :jvm)))

A row, a table and a set

Run it and read the ten lines. Then add a fourth number to `nums`, add a `:city` key to the map, and print how many values the set holds after adding `:jvm` to it.

Output
Click "Run" to execute your code

Reading a slot has two spellings. (get nums 9) answers nil, and (nth nums 9) complains loudly instead. Choose by meaning: get when a missing value is ordinary and you have a default for it, nth when a position past the end is a mistake you want to hear about.

Three collection mistakes are worth avoiding. A set literal refuses duplicates at read time: #{1 1 2} is an error, not a quiet two-item set, so build the set with (set [1 1 2]) when the values may repeat. conj adds to the end of a vector but the front of a list, so the same call means different things depending on the collection. And a map’s printed order is not a promise — if the output must read the same every run, sort the keys before printing.

Try these next

  • Build a vector of three names, add a fourth with conj, and print both the old and the new value.
  • Make a map with two keys, read a missing key with a default, then add it with assoc.
  • Build a set from [1 1 2] with set and print its count.

A shelf, a stock map and a guest list

Run it and read the ten lines. Then add a fourth name to the shelf, an `:eggs` entry to the map, and a second repeated guest to the vector before it becomes a set.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

In a vector written [10 20 30], which call gives 20?