
Accessing and updating elements from collections
In this recipe, we will teach you how to access elements and update elements in collections.
Getting ready
You only need REPL, as described in the recipe in Chapter 1, Live Programming with Clojure, and no additional libraries. Start REPL so that you can review the sample code in this recipe.
How to do it...
Let's start with accessing collections.
Accessing collections using the nth function
nth
gets the nthelement from collections. The second argument of nth
starts from 0 and throws an exception if the second argument is larger than the number of elements minus 1:
(nth [1 2 3 4 5] 1) ;;=> 2 (nth '("a" "b" "c" "d" "e") 3) ;;=> "d" (nth [1 2 3] 3) ;;=> IndexOutOfBoundsException clojure.lang.PersistentVector.arrayFor (PersistentVector.java:153)
If you would like to avoid such an exception, use the third argument as the return value:
(nth [1 2 3] 3 nil) ;;=> nil
Notice that nth
does not work with maps and sets.
Accessing maps or sets using get
get
accesses maps and sets using a key, and if there is a corresponding key in a map or set, it returns its value. If there is not the same key in a map or set, it returns nil or the third argument:
(get {:a 1 :b 2 :c 3 :d 4 :e 5} :c) ;;=> 3 (get {:a 1 :b 2 :c 3 :d 4 :e 5} :f) ;;=> nil (get {:a 1 :b 2 :c 3 :d 4 :e 5} :f :not-found) ;;=> :not-found (get #{:a :b :c} :c) ;;=> :c (get #{:a :b :c} :d) ;;=> nil (get #{:a :b :c} :d :not-found) ;;=> :not-found
Maps, sets, and keywords are functions to access collections
It is an idiomatic Clojure approach to use maps or sets as the first argument. The following code is the same as using get
:
` ;;=> :not-found
Using keywords is also Clojure-idiomatic. This is the same as get
:
(:c {:a 1 :b 2 :c 3 :d 4 :e 5}) ;:=> 3 (:f {:a 1 :b 2 :c 3 :d 4 :e 5}) ;;=> nil (:f {:a 1 :b 2 :c 3 :d 4 :e 5} :not-found) ;;=> :not-found
get
for maps returns the key if the key exists in the elements. It returns nil
if there is no matching key:
(get #{:banana :apple :strawberry :orange :melon} :orange) ;;=> :orange (get #{:banana :apple :strawberry :orange :melon} :grape) ;;=> nil (get #{:banana :apple :strawberry :orange :melon} :grape :not-found) ;;=> :not-found
We can use a set or a keyword as the first argument, but there is a third argument in this set:
(#{:banana :apple :strawberry :orange :melon} :orange) ;;=> :orange (#{:banana :apple :strawberry :orange :melon} :grape) ;;=> nil (:orange #{:banana :apple :strawberry :orange :melon}) ;;=> :orange ( :grape #{:banana :apple :strawberry :orange :melon}) ;;=> nil
Accessing a collection using second, next, ffirst, and nfirst
second
returns the second element from a collection:
(second [1 2 3 4 5]) ;;=> 2 (second '()) ;;=> nil
The behavior of next
is almost the same as rest
, but next
returns nil
when the result is empty:
(next [1]) ;;=> nil (rest [1]) ;;=> ()
ffirst
returns the first element of the first element:
(ffirst [[1 2 3] 4 [3 5 6]]) ;;=> 1 (ffirst {:a 1 :b 2}) ;;=> :a
This is equivalent to the following code:
(first (first [[1 2 3] 4 [3 5 6]])) ;;=> 1 (first (first {:a 1 :b 2})) ;;=> :a
nfirst
returns the next element of the first element:
(nfirst [[1 2] [3 4][5 6]]) ;;=> (2) (nfirst {:a 1 :b 2}) ;;=> (1)
Using update to update collections
update
updates the matched value using the function specified by the third element:
(update {:a 1 :b 2 :c 3} :a inc) ;;=> {:a 2, :b 2, :c 3}
How it works...
Maps, sets, and vectors implement clojure.lang.IFn
. Clojure functions such as +
implement IFn
. This is the reason why maps and sets can be functions. ifn?
tests whether an argument implements clojure.lang.IFn
:
(ifn? +) ;;=> true (ifn? []) ;;=> true (ifn? {}) ;;=> true (ifn? #{}) ;;=> true (ifn? :a) ;;=> true (ifn? '()) ;;=> false (ifn? 1) ;;=> false
Vectors and maps are functions, but lists and integers are not functions.
There's more...
Here, we will teach you some functions that are useful for accessing and updating collections.
Using get for vectors
Like with maps and sets, get
works with vectors:
(get ["a" "b" "c" "d" "e"] 3) ;;=> "d" (get ["a" "b" "c" "d" "e"] 5) ;;=> nil (get ["a" "b" "c" "d" "e"] 5 :not-found) ;;=> :not-found
Vectors are also functions:
(["a" "b" "c" "d" "e"] 3) ;;=> "d" (["a" "b" "c" "d" "e"] 5) ;;=> IndexOutOfBoundsException clojure.lang.PersistentVector.arrayFor (PersistentVector.java:153)
Using get
for lists always returns nil
. Never do that:
(get '(1 2 3) 1) ;;=> nil
Using collections as keys in maps
Unlike Java, Python, and Ruby, Clojure can use collection types as keys. The next map type uses maps as keys and finds the value using a map key:
(def location {{:x 1 :y 1} "Nico" {:x 1 :y 2} "John" {:x 2 :y 1} "Makoto" {:x 2 :y 2} "Tony"} ) ;;=> #'chapter03.core/location (location {:x 2 :y 2} ) ;;=> "Tony"
This is useful when the key is a spatial dimension.
Using get-in
get-in
associates given keys in a vector with a collection and returns a value of the matched element. get-in
takes the first argument as a collection and the second argument usually has multiple keys as vectors that you want to look up. We will go back to Conan-Doyle's biography and see how get-in
works:
(def biography-of-konan-doyle {:name "Arthur Ignatius Conan Doyle" :born "22-May-1859" :died "7-July-1930" :occupation ["novelist" "short story writer" "poet" "physician"] :nationality "scotish" :citizenship "United Kingdom" :genre ["Detective fiction", "fantasy", "science fiction", "historical novels", "non-fiction"] :notable-works ["Stories of Sherlock Holmes" "The Lost World"] :spouse ["Louisa Hawkins" "Jean Leckie"] :no-of-children 5 } )(get-in biography-of-konan-doyle [:genre 2]) ;;=> "science fiction"
In the preceding example, the second argument of get-in
is [:genre 2]
. get-in
looks for :genre
in the var
of biography-of-konan-doyle
and finds the value as follows:
["Detective fiction" "fantasy" "science fiction" "historical novels" "non-fiction"]
Then it looks for the third element in the vector and returns "science fiction"
. The following code is the equivalent code and returns the same result:
(get (get biography-of-konan-doyle :genre) 2) ;;=> "science fiction"
Using assoc-in
assoc-in
is similar to get-in
. It returns a collection that replaces the matched value with the third argument. If there are no matched keys, assoc-in
inserts a new value in a new location:
(assoc-in {:a {:b 1 :c 2} :d 3} [:a :c] 1) ;;=> {:a {:b 1, :c 1}, :d 3} (assoc-in {:a {:b 1 :c 2} :d 3} [:a :d] 1) ;;=> {:a {:b 1, :c 2, :d 1}, :d 3}
Using update-in
update-in
is similar to get-in
, but it only updates existing data. It is also different in that the third argument for update-in
is a function.
In the next sample, the first update-in
expression increments the matched value. The second expression sets a constant value, 10
, using the constantly
function:
(update-in {:a {:b 1 :c 2} :d 3} [:a :c] inc) ;;=> {:a {:b 1, :c 3}, :d 3} (update-in {:a {:b 1 :c 2} :d 3} [:a :c] (constantly 10)) ;;=> {:a {:b 1, :c 10}, :d 3}