Using the Sequence Library

The Clojure sequence library provides a rich set of functionality that can work with any sequence. If you come from an object-oriented background where nouns rule, the sequence library is truly “Revenge of the Verbs.” The functions provide a rich backbone of functionality that can take advantage of any data structure that obeys the basic first/rest/cons contract.

The following functions are grouped into four broad categories:

  • Functions that create sequences
  • Functions that filter sequences
  • Sequence predicates
  • Functions that transform sequences

These divisions are somewhat arbitrary. Since sequences are immutable, most of the sequence functions create new sequences. Some of the sequence functions both filter and transform. Nevertheless, these divisions provide a rough road map through a large library.

Creating Sequences

Clojure provides a number of functions that create sequences. range produces a sequence from a start to an end, incrementing by step each time.

 (range start? end? step?)

Ranges include their start but not their end. If you do not specify them, start defaults to zero, end defaults to positive infinity, and step defaults to 1. Try creating some ranges at the REPL:

 (range 10) ​;; end only
 -> (0 1 2 3 4 5 6 7 8 9)
 
 (range 10 20) ​;; start + end
 -> (10 11 12 13 14 15 16 17 18 19)
 
 (range 1 25 2) ​;; step by 2
 -> (1 3 5 7 9 11 13 15 17 19 21 23)
 
 (range 0 -1 -0.25) ​;; negative step
 -> (0 -0.25 -0.5 -0.75)
 
 (range 1/2 4 1) ​;; ratios
 -> (1/2 3/2 5/2 7/2)

The repeat function repeats an element x n times:

 (repeat n x)

Try to repeat some items from the REPL:

 (repeat 5 1)
 -> (1 1 1 1 1)
 
 (repeat 10 ​"x"​)
 -> (​"x"​ ​"x"​ ​"x"​ ​"x"​ ​"x"​ ​"x"​ ​"x"​ ​"x"​ ​"x"​ ​"x"​)

Both range and repeat represent ideas that can be extended infinitely.

iterate begins with a value x and continues forever, applying a function f to each value to calculate the next.

 (iterate f x)

If you begin with 1 and iterate with inc, you can generate the whole numbers:

 (take 10 (iterate inc 1))
 -> (1 2 3 4 5 6 7 8 9 10)

Since the sequence is infinite, you need another new function to help you view the sequence from the REPL.

 (take n sequence)

take returns a lazy sequence of the first n items from a collection and provides one way to create a finite view onto an infinite collection.

The whole numbers are a pretty useful sequence to have around, so let’s def them for future use:

 (​def​ whole-numbers (iterate inc 1))
 -> #​'user/whole-numbers

When called with a single argument, repeat returns a lazy, infinite sequence:

 (repeat x)

Try repeating at the REPL. Don’t forget to wrap the result in a take:

 (take 20 (repeat 1))
 -> (1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1)

The cycle function takes a collection and cycles it infinitely:

 (cycle coll)

Try cycling some collections at the REPL:

 (take 10 (cycle (range 3)))
 -> (0 1 2 0 1 2 0 1 2 0)

The interleave function takes multiple collections and produces a new collection that interleaves values from each collection until one of the collections is exhausted.

 (interleave & colls)

When one of the collections is exhausted, the interleave stops. So, you can mix finite and infinite collections:

 (interleave whole-numbers [​"A"​ ​"B"​ ​"C"​ ​"D"​ ​"E"​])
 -> (1 ​"A"​ 2 ​"B"​ 3 ​"C"​ 4 ​"D"​ 5 ​"E"​)

Closely related to interleave is interpose, which returns a sequence with each of the elements of the input collection segregated by a separator:

 (interpose separator coll)

You can use interpose to build delimited strings:

 (interpose ​","​ [​"apples"​ ​"bananas"​ ​"grapes"​])
 -> (​"apples"​ ​","​ ​"bananas"​ ​","​ ​"grapes"​)

interpose works nicely with (apply str ...) to produce output strings:

 (apply str (interpose ​","​ [​"apples"​ ​"bananas"​ ​"grapes"​]))
 -> ​"apples,bananas,grapes"

The (apply str (interpose separator sequence)) idiom is common enough that Clojure provides a performance-optimized version as clojure.string/join:

 (join separator sequence)

Use clojure.string/join to comma-delimit a list of words:

 (require '[clojure.string :refer [join]])
 (join ​,​ [​"apples"​ ​"bananas"​ ​"grapes"​])
 -> ​"apples,bananas,grapes"

For each collection type in Clojure, there is a function that takes an arbitrary number of arguments and creates a collection of that type:

 (list & elements)
 (vector & elements)
 (hash-set & elements)
 (hash-map key-1 val-1 ...)

hash-set has a cousin set that works a little differently: set expects a collection as its first argument:

 (set [1 2 3])
 -> #{1 2 3}

hash-set takes a variable list of arguments:

 (hash-set 1 2 3)
 -> #{1 2 3}

vector also has a cousin, vec, which takes a single collection argument instead of a variable argument list:

 (vec (range 3))
 -> [0 1 2]

Now that you have the basics of creating sequences, you can use other Clojure functions to filter and transform them.

Filtering Sequences

Clojure provides a number of functions that filter a sequence, returning a subsequence of the original sequence. The most basic of these is filter:

 (filter pred coll)

filter takes a predicate and a collection and returns a sequence of objects for which the filter returns true (when interpreted in a Boolean context). You can filter the whole-numbers from the previous section to get the odd numbers or the even numbers:

 (take 10 (filter even? whole-numbers))
 -> (2 4 6 8 10 12 14 16 18 20)
 
 (take 10 (filter odd? whole-numbers))
 -> (1 3 5 7 9 11 13 15 17 19)

You can take from a sequence while a predicate remains true with take-while:

 (take-while pred coll)

For example, to take all the characters in a string up to the first vowel, we can define some useful helper functions:

 (​def​ vowel? #{​aeiou​})
 (​def​ consonant? (complement vowel?))

Then use those predicates to take the characters from the string up to the first vowel:

 (take-while consonant? ​"the-quick-brown-fox"​)
 -> (​ ​ ​h​)

A couple of interesting things are happening here:

  • Sets act as functions that look up a value in the set and return either the value or nil if not found. So, you can read #{aeiou} as “the function that tests to see whether its argument is a vowel.”

  • complement reverses the behavior of another function. Here we create consonant? by defining it as the function that is the complement of vowel?.

The opposite of take-while is drop-while:

 (drop-while pred coll)

drop-while drops elements from the beginning of a sequence while a predicate is true and then returns the rest. You could use drop-while to drop all leading non-vowels from a string:

 (drop-while consonant? ​"the-quick-brown-fox"​)
 -> (​e​ ​-​ ​q​ ​u​ ​i​ ​c​ ​k​ ​-​ ​​ ​ ​ ​o​ ​w​ ​ ​ ​-​ ​f​ ​o​ ​x​)

split-at and split-with will split a collection into two collections:

 (split-at index coll)
 (split-with pred coll)

split-at takes an index, and split-with takes a predicate:

 (split-at 5 (range 10))
 ->[(0 1 2 3 4) (5 6 7 8 9)]
 
 (split-with #(<= % 10) (range 0 20 2))
 ->[(0 2 4 6 8 10) (12 14 16 18)]

All the take-, split-, and drop- functions return lazy sequences, of course.

Sequence Predicates

Filter functions take a predicate and return a sequence. Closely related are the sequence predicates. A sequence predicate asks how some other predicate applies to every item in a sequence. For example, the every? predicate asks whether some other predicate is true for every element of a sequence.

 (every? pred coll)
 (every? odd? [1 3 5])
 -> true
 
 (every? odd? [1 3 5 8])
 -> false

A lower bar is set by some:

 (some pred coll)

some returns the first non-false value for its predicate or returns nil if no element matched:

 (some even? [1 2 3])
 -> true
 
 (some even? [1 3 5])
 -> nil

Notice that some does not end with a question mark. some is not a predicate, although it’s often used like one. some returns the actual value of the first match instead of true. The distinction is invisible when you pair some with even?, since even? is itself a predicate. To see a non-true match, try using some with identity to find the first logically true value in a sequence:

 (some identity [nil false 1 nil 2])
 -> 1

A common use of some is to perform a linear search to see if a sequence contains a matching element, which is typically written as a set of a single element. For example to see if a sequence contains the value 3:

 (some #{3} (range 20))
 -> 3

Note that the value returned is the value in the sequence, which would act as a truthy value if this was used as a conditional test.

The behavior of the other predicates is obvious from their names:

 (not-every? pred coll)
 (not-any? pred coll)

Not every whole number is even:

 (not-every? even? whole-numbers)
 -> true

But it would be a lie to claim that not any whole number is even:

 (not-any? even? whole-numbers)
 -> false

Note that we picked questions to which we already knew the answer. In general, you have to be careful when applying predicates to infinite collections. They might run forever.

Transforming Sequences

Transformation functions transform the values in the sequence. The simplest transformation is map:

 (map f coll)

map takes a source collection coll and a function f, and it returns a new sequence by invoking f on each element in the coll. You could use map to wrap every element in a collection with an HTML tag.

 (map #(format ​"<p>%s</p>"​ %) [​"the"​ ​"quick"​ ​"brown"​ ​"fox"​])
 -> (​"<p>the</p>"​ ​"<p>quick</p>"​ ​"<p>brown</p>"​ ​"<p>fox</p>"​)

map can also take more than one collection argument. f must then be a function of multiple arguments. map will call f with one argument from each collection, stopping whenever the smallest collection is exhausted:

 (map #(format ​"<%s>%s</%s>"​ %1 %2 %1)
 [​"h1"​ ​"h2"​ ​"h3"​ ​"h1"​] [​"the"​ ​"quick"​ ​"brown"​ ​"fox"​])
 -> (​"<h1>the</h1>"​ ​"<h2>quick</h2>"​ ​"<h3>brown</h3>"
 "<h1>fox</h1>​"​)

Another common transformation is reduce:

 (reduce f coll)

f is a function of two arguments. reduce applies f on the first two elements in coll and then applies f to the result and the third element, and so on. reduce is useful for functions that “total up” a sequence in some way. You can use reduce to add items:

 (reduce + (range 1 11))
 -> 55

or to multiply them:

 (reduce * (range 1 11))
 -> 3628800

You can sort a collection with sort or sort-by:

 (sort comp? coll)
 (sort-by a-fn comp? coll)

sort sorts a collection by the natural order of its elements, where sort-by sorts a sequence by the result of calling a-fn on each element:

 (sort [42 1 7 11])
 -> (1 7 11 42)
 
 (sort-by #(.toString %) [42 1 7 11])
 -> (1 11 42 7)

If you don’t want to sort by natural order, you can specify an optional comparison function comp for either sort or sort-by:

 (sort > [42 1 7 11])
 -> (42 11 7 1)
 
 (sort-by :grade > [{:grade 83} {:grade 90} {:grade 77}])
 -> ({:grade 90} {:grade 83} {:grade 77})

The granddaddy of all filters and transformations is the list comprehension. A list comprehension creates a list based on an existing list, using set notation. In other words, a comprehension states the properties that the result list must satisfy. In general, a list comprehension will consist of the following:

  • Input list(s)
  • Placeholder bindings for elements in the input lists
  • Predicates on the elements
  • An output form that produces output from the elements of the input lists that satisfy the predicates

Of course, Clojure generalizes the notion of list comprehension to sequence comprehension. Clojure comprehensions use the for macro. Note that the list comprehension for has nothing to do with the for loop found in imperative languages.

 (​for​ [binding-form coll-expr filter-expr? ...] expr)

for takes a vector of binding-form/coll-exprs, plus optional filter-exprs, and then yields a sequence of exprs.

List comprehension is more general than functions such as map and filter and can in fact emulate most of the filtering and transformation functions described earlier.

You can rewrite the previous map example as a list comprehension:

 (​for​ [word [​"the"​ ​"quick"​ ​"brown"​ ​"fox"​]]
  (format ​"<p>%s</p>"​ word))
 -> (​"<p>the</p>"​ ​"<p>quick</p>"​ ​"<p>brown</p>"​ ​"<p>fox</p>"​)

This reads almost like English: “For [each] word in [a sequence of words], format [according to format instructions].”

Comprehensions can emulate filter using a :when clause. You can pass even? to :when to filter the even numbers:

 (take 10 (​for​ [n whole-numbers :when (even? n)] n))
 -> (2 4 6 8 10 12 14 16 18 20)

A :while clause continues the evaluation only while its expression holds true:

 (​for​ [n whole-numbers :while (even? n)] n)
 -> ()

The real power of for comes when you work with more than one binding expression. For example, you can express all possible positions on a chessboard in algebraic notation by binding both rank and file:

 (​for​ [file ​"ABCDEFGH"
  rank (range 1 9)]
  (format ​"%c%d"​ file rank))
 -> (​"A1"​ ​"A2"​ ... elided ... ​"H7 ""H8"​)

Clojure iterates over the rightmost binding expression in a sequence comprehension first and then works its way left. Because rank is listed to the right of file in the binding form, rank iterates faster. If you want files to iterate faster, you can reverse the binding order and list rank first:

 (​for​ [rank (range 1 9)
  file ​"ABCDEFGH"​]
  (format ​"%c%d"​ file rank))
 -> (​"A1"​ ​"B1"​ ... elided ... ​"G8"​ ​"H8"​)

In many languages, transformations, filters, and comprehensions do their work immediately. Do not assume this in Clojure. Most sequence functions do not traverse elements until you actually try to use them.

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

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