Part 6 — A complete beginner's guide to Computer Programming with Clojure: Lists, Sets, Vectors, and Maps.
In this lesson, we will demonstrate all the different ways you can store and retrieve stuff with Clojure.
Lists
A list is simply a collection of data items. There does not have to be any relation between the items and you can mix together items. For example, a list can contain numbers and strings.
Type all the following in a REPL and observe the results.
(println (list “Yoda” 401.0 true))
Here we have a list of three items, string, a floating-point number, and a boolean value: true. The list command in the inner brackets creates our list and then the outer brackets contain the println command to print the results to the terminal. Recall, as nothing is actually evaluated, you will see nil.
(println (first (list 20 30 40)))
Here the first item (20) is printed.
(println (rest (list 20 30 40)))
This prints all the rest i.e., everything but the first item.
(println (nth (list 20 30 40) 1))
This prints the number in position 1. 20 is located in the first position of 0 (zero). Therefore, position 1 holds 30.
(println (list* 9 8 [ 7 6 ]))
This command creates a new list from two separate items. The items in the square brackets are appended to the items after the star *.
(println (cons 2 (list 4 6)))
This is another way to create a new list. Here, cons simply places a new item to the front of an existing list to make a new list.
Sets
A set is different to a list, in that a set can only contain unique values. This can be a powerful tool in the Coder’s arsenal. For instance, I once needed to create code to identify a specific device present in more than one location. I treated each location as a set. For each location, all devices present displayed a unique identifier in the form of a hardware MAC address. Only one specific device (the one I was after) would be present in all locations. In short, the program looked for the one MAC address present in all sets.
Again, type all the following in a REPL and observe the results.
(println (set ‘(1 1 1 2 2 2 3 3 3 4 4 4)))
To prove the point, our set only contains unique values and is prefixed with a hash #.
(println (get (set ‘(3 7 5 30)) 30))
Let’s just get the number 30 from our set.
(println (conj (set ‘(3 7 5 30)) 50))
Conjoin (combine/join) two sets together to make one set containing all the values.
(println (contains? (set ‘(3 7 5 30)) 30))
Does the set contain the number 30? It does!
(println (disj (set ‘(3 7 5 30)) 30))
A new set is created after removing the number 30.
Vectors
In many ways, a Vector is similar to a List. Both a List and a Vector are heterogeneous i.e. you can mix together different elements like numbers and strings. Also, both are indexed so you can easily identify the nth item. The main difference is a Vector stores the elements contiguously in computer memory i.e. next to each other. This makes Vectors fast!
(println (get (vector 3 “Skywalker” 5 30) 1))
Get the element indexed in position 1.
(println (conj (vector 3 7 5 30) 50))
For this Vector, we will bring in the number 50.
(println (pop (vector 3 7 5 30)))
The pop command is applied directly to the computer memory and takes away the last element in memory.
(println (subvec (vector 3 7 5 30) 1 2 ))
The subvector command creates a new vector from the element stored in the first index (1) and just before but not including the next index point (2).
(println (subvec (vector 3 7 5 30) 1 4 ))
This is interesting as our subvector has captured all elements to the end. If you had used an index range of 1 5, you would have got an error indicating the result is out of range.
Maps
A Map is a powerful tool for storing and retrieving data. A Map is created when elements are mapped to a key, known as a key-value pair. In short, you can look up the key to find the associated value.
Test out the following code snippets in a REPL.
Try to figure out how key-value pairs can be applied to a programming problem.
(println (hash-map “Name” “Yoda” “Age” 900))
A simple hash-map created.
(println (sorted-map :color “green” :species “Minch” :first-name “Yoda” ))
A sorted-map orders the map by the index. In this case, it’s in alphabetic order: c, f, s.
(println (get (hash-map “name” “Yoda”) “name” ))
Get the value associated with the index of name.
(println (find (hash-map “name” “Yoda” “age” 900) “name”))
Similar to get but returns the key as well and delivers the result as a vector.
(println (contains? (hash-map “name” “Yoda” “age” “900”) “age”))
Check to see if the map contains the string “age”.
(println (keys (hash-map “name” “Yoda” “age” “900”)))
Pull out just the keys.
(println (vals (hash-map “name” “Yoda” “age” “900”)))
Pull out just the values.
Simple Database
Let’s create a simple database. Our database will take a book title and will collect together any associated data. For example, the book title will have an ISBN, Author, Publish year etc. Interestingly, books always have a title. However, they may have multiple authors or no author and referred to as anonymous. In addition, ISBN numbers change with publication dates, book versions, publishers, etc. Also, people often remember titles more often than they remember the author, least of all the ISBN number!
Our database will take a map of keys and values. Keys begin with a colon : and are paired with a value. In the example, the key pairs are nested under a primary value.
Type the following into a REPL
(def Library{“The Caves of Steel” { :ISBN “0–553–29340–0” :Title “The Caves of Steel” :Author “Isaac Asimov” :Year “1954”}“After Yesterdays Crash” { :ISBN “0–14–024085–3” :Title “After Yesterdays Crash” :Author “William Gibson”}})
Our database is called Library, hence def Library. The word def is a keyword that is used to save our data. In our example, def saves, or associates, the data to a symbol called Library.
To be a database, our program must possess CRUD functionality. CRUD stands for Create, Read, Update, and Delete.
READ
Let’s look for the author associated with the title ‘The Caves of Steel’.
(get-in Library [“The Caves of Steel” :Author] )
Here, we use the built-in function get-in to search for a Map vector with the string ‘The Caves of Steel’ and then any nested value attributed to the Key :Author. As expected, “Isaac Asimov” will be returned.
Should we look for something that is not there?
e.g. (get-in Library [“The Caves of Steel” :Publisher]), the program would only return nil. Recall, Clojure always has to return something even if it is nil.
We could combine commands to retrieve more data like so
(println “The author is “
(get-in Library [“The Caves of Steel” :Author] ) “published “
(get-in Library [“The Caves of Steel” :Year] ))
Unfortunately, our function has a lot of repetition e.g. ‘get-in’, ‘The Caves of Steel’, Library, etc.
let
The keyword let allows us to associate values and even other functions. For example, println is a function.
Look at the following
(let [p println] (p “Hello”))
Here, let assigns the function println to p. When p is called, it actually calls the function println.
IMPORTANT — def versus let
However, let only works inside the function where it is applied. If you call p outside of the function i.e. outside of the outermost brackets, it is plain and simple p. However, when we use a keyword like def, we can call it from outside its function.
Have a look at the function below
(let [x “The Caves of Steel” y get-in z Library]
(println “The author is “(y z [x :Author] ) “published “
(y z [x :Year] )))
To save us typing, we have used let to assign x y z to “The Caves of Steel”, the function get-in, and our symbol Library. Note, x y z is only assigned these values inside the function. As Library is a symbol created with the keyword def, it can be used anywhere in the program.
To better explain def, recall when we used let inside a function to substitute p for println
(let [p println] (p “Hello”))
To do the same with def, we have to be more specific. Look at the following.
(def p (fn [x] (println x)))
Reading from left to right, we will use def to define a symbol called p. Symbol p will be a function fn that takes just one parameter which we will call x [x]. This single parameter x will have the function println applied to it.
As a result,
(p “Hello”)
Hello
nil
Now look at the following
(def G (fn [x y] (get-in x y)))
Reading from left to right, we will use def to define a symbol called G. Symbol G will be a function fn that takes two parameters which we will call x and y [x y]. The parameters x and y will have the function get-in applied to each of them respectively.
Here is the function applied, where the x parameter takes the symbol Library. Parameter y takes the vector [“The Caves of Steel” :Author].
(G Library [“The Caves of Steel” :Author])"Isaac Asimov"
Regarding this example, had we omitted the symbol Library and written
(G [“The Caves of Steel” :Author])
we would have received an error. Recall, the G function has two parameters.
The error message tells us that we only passed one argument (args) to a function with two parameters x & y. In other words, the function expected two arguments. An argument is anything that at passed to a parameter. So in our example, the function fn [x y] expected two arguments, Library and [“The Caves of Steel” :Author]).
To allow to use G without the symbol Library, we would have to re-write our function to take only one parameter. We would then need to include the symbol Library on our function
(def G (fn [x] (get-in Library x )))
Reading from left to right, we will use def to define a symbol called G. Symbol G will be a function fn that takes one parameter which we will call x [x ]. The parameters x will have the function get-in Library applied to it.
(G [“The Caves of Steel” :Author])"Isaac Asimov"
Going back to our let statement. we have the following
(let [x “The Caves of Steel”] (println “The author is” (G [x :Author]) “published” (G [x :Year])))"The author is Isaac Asimov published 1954"
nil
As you can see, this is very much abbreviated.
UPDATE
Returning to our database
(def Library{“The Caves of Steel” { :ISBN “0–553–29340–0” :Title “The Caves of Steel” :Author “Isaac Asimov” :Year “1954”}“After Yesterdays Crash” { :ISBN “0–14–024085–3” :Title “After Yesterdays Crash” :Author “William Gibson”}})
Let’s update ‘After Yesterdays Crash” by adding in the year 1970.
(assoc-in Library [“After Yesterdays Crash” :Year] “1970”)
Easy, now just fetch the data with
(get-in Library [“After Yesterdays Crash” :Year])
So. what just happened?
MUTABILITY — with an atom!
Clojure is a functional language and is designed to hold pieces of stored data in a static, unchangeable, form. In short, when you create anything with def it is created to be immutable. In other words, once it has been created, it cannot. be changed. Not much good for creating a database!
Nevertheless, you get around this by using the keyword atom like so
(def Library (atom{“The Caves of Steel” { :ISBN “0–553–29340–0” :Title “The Caves of Steel” :Author “Isaac Asimov” :Year “1954”}“After Yesterdays Crash” { :ISBN “0–14–024085–3” :Title “After Yesterdays Crash” :Author “William Gibson”}}))
To work with our database now, you have to make some slight changes to account for the fact that you are now working with the keyword atom.
For instance, when reading your database, apply @ to Library like so
(get-in @Library [“The Caves of Steel” :Author])
Now, let’s return to our original problem. Let’s update our database by adding the year 1970 to the book titled, “After Yesterdays Crash”. To do this we will use another necessary keyword, swap!
(swap! Library assoc-in [“After Yesterdays Crash” :Year] “1970”)
Let’s check it has updated
(get-in @Library [“After Yesterdays Crash” :Year])
The database can be listed in full by simply typing @Library
This how our database looks now
{“The Caves of Steel” { :ISBN “0–553–29340–0”, :Title “The Caves of Steel”, :Author “Isaac Asimov”, :Year “1954”},“After Yesterdays Crash” { :ISBN “0–14–024085–3”, :Title “After Yesterdays Crash”, :Author “William Gibson”, :Year “1970”}}
CREATE
At this point, we can read our database and we can update. Now, we will add a completely new book. The Book will be ‘The War of the Worlds’ by H G Wells.
Type the following. This will create a new entry, including the ISBN.
(swap! Library assoc-in [“The War of the Worlds” :ISBN] “978–1604502442”)
Let’s add the year
(swap! Library assoc-in [“The War of the Worlds” :Year] “1898”)
Now check it’s there
(contains? @Library “The War of the Worlds” )
Our Library database should like this
{“The Caves of Steel” { :ISBN “0–553–29340–0”, :Title “The Caves of Steel”, :Author “Isaac Asimov”, :Year “1954”},“After Yesterdays Crash” { :ISBN “0–14–024085–3”, :Title “After Yesterdays Crash”,` :Author “William Gibson”, :Year “1970”},`“The War of the Worlds” { :ISBN “978–1604502442”,` :Year “1898”}}
DELETE
Firstly, let’s check what we still have recorded against the book title, ‘The War of the Worlds’.
(get-in @Library [“The War of the Worlds”]){:ISBN "978-1605402442", :Year "1898"}
We will delete the Year
(swap! Library update-in [“The War of the Worlds” ] dissoc :Year)
A quick check will reveal it has gone
Our Library database should now look like this
{“The Caves of Steel” { :ISBN “0–553–29340–0”, :Title “The Caves of Steel”, :Author “Isaac Asimov”, :Year “1954”},“After Yesterdays Crash” { :ISBN “0–14–024085–3”, :Title “After Yesterdays Crash”, :Author “William Gibson”, :Year “1970”},“The War of the Worlds” { :ISBN “978–1604502442”, }}
Finally, let’s completely remove a complete book and its associated data from our Library database. We will completely remove ‘After Yesterdays Crash’.
(swap! Library dissoc “After Yesterdays Crash”)
After demonstrating all CRUD functionality, our database looks like this
{“The Caves of Steel” { :ISBN “0–553–29340–0”, :Title “The Caves of Steel”, :Author “Isaac Asimov”, :Year “1954”},“The War of the Worlds” { :ISBN “978–1604502442”}}
SUMMARY
In the first half of this Post, we introduced a number of ways to store and retrieve items with Clojure. It’s also worth noting the subtle representations given to stored items. For instance, a set is prefixed with # and is contained inside curly {} brackets. Whereas, a vector is represented with square [] brackets.
Further along, we looked at Maps and the principle of indexing via key-value pairs. Later, we extended our knowledge to look at a simple database. I also introduced some database-specific terminology, CRUD.
Another learning point concerned how def will create a static variable that can be called from outside a function. As opposed let, as let will only work inside the function it was created.
A further look at functions introduced the difference between a parameter and an argument. We also had our first look at an error message (arity) and explained it in terms of parameters and arguments.
Near the end, we briefly touched upon the concept of Mutability and Atoms. Part 7 will attempt to explain the subjects of Atoms and Mutability in greater detail.
To get the most out of this Post, practice all the examples but try and think of other applications where these techniques can be applied. For example, when would a list be preferable to a vector?