Calling Structure-Specific Functions

Clojure’s sequence functions allow you to write very general code. Sometimes you’ll want to be more specific and take advantage of the characteristics of a specific data structure. Clojure includes functions that specifically target lists, vectors, maps, structs, and sets.

We’ll take a quick tour of some of these structure-specific functions next. For a complete list of structure-specific functions in Clojure, see the Data Structures section of the Clojure website.[20]

Functions on Lists

Clojure supports the traditional names peek and pop for retrieving the first element of a list and the remainder, respectively:

 (peek coll)
 (pop coll)

Give a simple list a peek and pop:

 (peek '(1 2 3))
 -> 1
 
 (pop '(1 2 3))
 -> (2 3)

peek is the same as first, but pop is not the same as rest. pop will throw an exception if the sequence is empty:

 (rest ())
 -> ()
 
 (pop ())
 -> java.lang.IllegalStateException​:​ Can​'t​ pop empty list

Functions on Vectors

Vectors also support peek and pop, but they deal with the element at the end of the vector:

 (peek [1 2 3])
 -> 3
 
 (pop [1 2 3])
 -> [1 2]

get returns the value at an index or returns nil if the index is outside the vector:

 (get [:a :b :c] 1)
 -> :b
 
 (get [:a :b :c] 5)
 -> nil

Vectors are themselves functions. They take an index argument and return a value, or they throw an exception if the index is out of bounds:

 ([:a :b :c] 1)
 -> :b
 
 ([:a :b :c] 5)
 -> java.lang.IndexOutOfBoundsException

assoc associates a new value with a particular index:

 (assoc [0 1 2 3 4] 2 :two)
 -> [0 1 :two 3 4]

subvec returns a subvector of a vector:

 (subvec avec start end?)

If end is not specified, it defaults to the end of the vector:

 (subvec [1 2 3 4 5] 3)
 -> [4 5]
 
 (subvec [1 2 3 4 5] 1 3)
 -> [2 3]

Of course, you could simulate subvec with a combination of drop and take:

 (take 2 (drop 1 [1 2 3 4 5]))
 -> (2 3)

The difference is that take and drop are general and can work with any sequence. On the other hand, subvec is much faster for vectors. Whenever a structure-specific function like subvec duplicates functionality already available in the sequence library, it’s probably there for performance. The documentation string for functions like subvec includes performance characteristics.

Functions on Maps

Clojure provides several functions for reading the keys and values in a map. keys returns a sequence of the keys, and vals returns a sequence of the values:

 (keys map)
 (vals map)

Try taking keys and values from a simple map:

 (keys {:sundance ​"spaniel"​, :darwin ​"beagle"​})
 -> (:sundance :darwin)
 
 
 (vals {:sundance ​"spaniel"​, :darwin ​"beagle"​})
 -> (​"spaniel"​ ​"beagle"​)

Note that while maps are unordered, both keys and vals are guaranteed to return the keys and values in the same order as a seq on the map.

get returns the value for a key or returns nil.

 (get map key value-if-not-found?)

Use your REPL to test that get behaves as expected for keys both present and missing:

 (get {:sundance ​"spaniel"​, :darwin ​"beagle"​} :darwin)
 -> ​"beagle"
 
 (get {:sundance ​"spaniel"​, :darwin ​"beagle"​} :snoopy)
 -> nil

There’s an approach that’s even simpler than get. Maps are functions of their keys. So you can leave out the get entirely, putting the map in function position at the beginning of a form:

 ({:sundance ​"spaniel"​, :darwin ​"beagle"​} :darwin)
 -> ​"beagle"
 
 ({:sundance ​"spaniel"​, :darwin ​"beagle"​} :snoopy)
 -> nil

Keywords are also functions. They take a collection as an argument and look themselves up in the collection. Since :darwin and :sundance are keywords, the earlier forms can be written with their elements in reverse order.

 (:darwin {:sundance ​"spaniel"​, :darwin ​"beagle"​} )
 -> ​"beagle"
 
 (:snoopy {:sundance ​"spaniel"​, :darwin ​"beagle"​} )
 -> nil

If you look up a key in a map and get nil back, you can’t tell whether the key was missing from the map or present with a value of nil. The contains? function solves this problem by testing for the mere presence of a key.

 (contains? map key)

Create a map where nil is a legal value:

 (​def​ score {:stu nil :joey 100})

:stu is present, but if you see the nil value, you might not think so:

 (:stu score)
 -> nil

If you use contains?, you can verify that :stu is in the game, although presumably not doing very well:

 (contains? score :stu)
 -> true

Another approach is to call get, passing in an optional third argument that will be returned if the key is not found:

 (get score :stu :score-not-found)
 -> nil
 
 (get score :aaron :score-not-found)
 -> :score-not-found

The default return value of :score-not-found makes it possible to distinguish that :aaron is not in the map, while :stu is present with a value of nil.

If nil is a legal value in map, use contains? or the three-argument form of get to test the presence of a key.

Clojure also provides several functions for building new maps:

  • assoc returns a map with a key/value pair added.
  • dissoc returns a map with a key removed.
  • select-keys returns a map, keeping only a specified set of keys.
  • merge combines maps. If multiple maps contain a key, the rightmost wins.

To test these functions, create some song data:

 (​def​ song {:name ​"Agnus Dei"
  :artist ​"Krzysztof Penderecki"
  :album ​"Polish Requiem"
  :genre ​"Classical"​})

Next, create various modified versions of the song collection:

 (assoc song :kind ​"MPEG Audio File"​)
 -> {:name ​"Agnus Dei"​, :album ​"Polish Requiem"​,
 :kind ​"MPEG Audio File"​, :genre ​"Classical"​,
 :artist ​"Krzysztof Penderecki"​}
 
 (dissoc song :genre)
 -> {:name ​"Agnus Dei"​, :album ​"Polish Requiem"​,
 :artist ​"Krzysztof Penderecki"​}
 
 (select-keys song [:name :artist])
 -> {:name ​"Agnus Dei"​, :artist ​"Krzysztof Penderecki"​}
 
 (merge song {:size 8118166, :time 507245})
 -> {:name ​"Agnus Dei"​, :album ​"Polish Requiem"​,
 :genre ​"Classical"​, :size 8118166,
 :artist ​"Krzysztof Penderecki"​, :time 507245}

Remember that song itself never changes. Each of these functions returns a new collection.

The most interesting map construction function is merge-with.

 (merge-with merge-fn & maps)

merge-with is like merge, except that when two or more maps have the same key, you can specify your own function for combining the values under the key. Use merge-with and concat to build a sequence of values under each key:

 (merge-with
  concat
  {:rubble [​"Barney"​], :flintstone [​"Fred"​]}
  {:rubble [​"Betty"​], :flintstone [​"Wilma"​]}
  {:rubble [​"Bam-Bam"​], :flintstone [​"Pebbles"​]})
  -> {:rubble (​"Barney"​ ​"Betty"​ ​"Bam-Bam"​),
  :flintstone (​"Fred"​ ​"Wilma"​ ​"Pebbles"​)}

Starting with three distinct collections of family members keyed by last name, the previous code combines them into one collection keyed by last name.

Functions on Sets

In addition to the set functions in the clojure.core namespace, Clojure provides a group of functions in the clojure.set namespace. To use these functions with unqualified names, call (require ’[clojure.set :refer :all]) from the REPL. For the following examples, you’ll also need the following vars:

 (​def​ languages #{​"java"​ ​"c"​ ​"d"​ ​"clojure"​})
 (​def​ beverages #{​"java"​ ​"chai"​ ​"pop"​})

The first group of clojure.set functions performs operations from set theory:

  • union returns the set of all elements present in either input set.

  • intersection returns the set of all elements present in both input sets.

  • difference returns the set of all elements present in the first input set, minus those in the second.

  • select returns the set of all elements matching a predicate.

Write an expression that finds the union of all languages and beverages:

 (union languages beverages)
 -> #{​"java"​ ​"c"​ ​"d"​ ​"clojure"​ ​"chai"​ ​"pop"​}

Next, try the languages that are not also beverages:

 (difference languages beverages)
 -> #{​"c"​ ​"d"​ ​"clojure"​}

If you enjoy terrible puns, you’ll like the fact that some things are both languages and beverages:

 (intersection languages beverages)
 -> #{​"java"​}

A number of languages can’t afford a name larger than a single character:

 (select #(= 1 (count %)) languages)
 -> #{​"c"​ ​"d"​}

Set union and set difference are part of set theory, but they’re also part of relational algebra, which is the basis for query languages such as SQL. Relational algebra consists of six primitive operators: set union and set difference (described earlier), plus rename, selection, projection, and cross product.

Clojure sets (and maps) have everything we need to implement a basic relational algebra system. We’ll use maps to describe each tuple (like a row in a relational database) and a set to contain all of the tuples in a relation (like a table in a relational database).

The following examples work against an in-memory database of musical compositions. First, we’ll define the data in our “database”, which are just sets of maps:

 (​def​ compositions
  #{{:name ​"The Art of the Fugue"​ :composer ​"J. S. Bach"​}
  {:name ​"Musical Offering"​ :composer ​"J. S. Bach"​}
  {:name ​"Requiem"​ :composer ​"Giuseppe Verdi"​}
  {:name ​"Requiem"​ :composer ​"W. A. Mozart"​}})
 (​def​ composers
  #{{:composer ​"J. S. Bach"​ :country ​"Germany"​}
  {:composer ​"W. A. Mozart"​ :country ​"Austria"​}
  {:composer ​"Giuseppe Verdi"​ :country ​"Italy"​}})
 (​def​ nations
  #{{:nation ​"Germany"​ :language ​"German"​}
  {:nation ​"Austria"​ :language ​"German"​}
  {:nation ​"Italy"​ :language ​"Italian"​}})

The rename function renames keys (database columns) based on a map from original names to new names.

 (rename relation rename-map)

Rename the compositions to use a title key instead of name:

 (rename compositions {:name :title})
 -> #{{:title ​"Requiem"​, :composer ​"Giuseppe Verdi"​}
  {:title ​"Musical Offering"​, :composer ​"J.S. Bach"​}
  {:title ​"Requiem"​, :composer ​"W. A. Mozart"​}
  {:title ​"The Art of the Fugue"​, :composer ​"J.S. Bach"​}}

The select function returns maps, for which a predicate is true, and is analogous to the WHERE portion of a SQL SELECT:

 (select pred relation)

Write a select expression that finds all the compositions whose title is "Requiem":

 (select #(= (:name %) ​"Requiem"​) compositions)
 -> #{{:name ​"Requiem"​, :composer ​"W. A. Mozart"​}
  {:name ​"Requiem"​, :composer ​"Giuseppe Verdi"​}}

The project function returns only the parts of maps that match a set of keys.

 (project relation keys)

project is similar to a SQL SELECT that specifies a subset of columns. Write a projection that returns only the name of the compositions:

 (project compositions [:name])
 -> #{{:name ​"Musical Offering"​}
  {:name ​"Requiem"​}
  {:name ​"The Art of the Fugue"​}}

The final relational primitive, which is a cross product, is the foundation for the various kinds of joins in relational databases. The cross product returns every possible combination of rows in the different tables. You can do this easily enough in Clojure with a list comprehension:

 (​for​ [m compositions c composers] (concat m c))
 -> ... 4 x 3 = 12 rows ...

Although the cross product is theoretically interesting, you’ll typically want some subset of the full cross product. For example, you might want to join sets based on shared keys:

 (join relation-1 relation-2 keymap?)

You can join the composition names and composers on the shared key :composer:

 (join compositions composers)
 -> #{{:name ​"Requiem"​, :country ​"Austria"​,
  :composer ​"W. A. Mozart"​}
  {:name ​"Musical Offering"​, :country ​"Germany"​,
  :composer ​"J. S. Bach"​}
  {:name ​"Requiem"​, :country ​"Italy"​,
  :composer ​"Giuseppe Verdi"​}
  {:name ​"The Art of the Fugue"​, :country ​"Germany"​,
  :composer ​"J. S. Bach"​}}

If the key names in the two relations don’t match, you can pass a keymap that maps the key names in relation-1 to their corresponding keys in relation-2. For example, you can join composers, which uses :country, to nations, which uses :nation. For example:

 (join composers nations {:country :nation})
 -> #{{:language ​"German"​, :nation ​"Austria"​,
  :composer ​"W. A. Mozart"​, :country ​"Austria"​}
  {:language ​"German"​, :nation ​"Germany"​,
  :composer ​"J. S. Bach"​, :country ​"Germany"​}
  {:language ​"Italian"​, :nation ​"Italy"​,
  :composer ​"Giuseppe Verdi"​, :country ​"Italy"​}}

You can combine the relational primitives. Perhaps you want to know the set of all countries that are home to the composer of a requiem. You can use select to find all the requiems, join them with their composers, and project to narrow the results to just the country names:

 (project
  (join
  (select #(= (:name %) ​"Requiem"​) compositions)
  composers)
  [:country])
 -> #{{:country ​"Italy"​} {:country ​"Austria"​}}

The analogy between Clojure’s relational algebra and a relational database is instructive. Remember, though, that Clojure’s relational algebra is a general-purpose tool. You can use it on any kind of set-relational data. And while you’re using it, you have the entire power of Clojure and Java at your disposal.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.68.18