Clojure Programming Cookbook
上QQ阅读APP看书,第一时间看更新

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}