Clojure sequences are abstract. In this chapter, we'll make sense of what exactly we mean by that, and learn about common operations.
We have been using the word "sequence" (or "seq") without a concrete example. This is because seq is not concrete, but an abstraction. In the
Syntax and Native Data Types chapter, we learned that lists, vectors, sets, and maps are sequences, because they implement the
ISeq interface. But what exactly do we mean by that?
Many languages come bundled with functions like
filter etc. In ES6, these functions work on arrays. The
map function, for example, takes an array and a function and returns a new array where the element at index i is the result of the function applied to element i of the input array.
If the input array is
[1, 2, 4, 5], and the function is
i => i + 1,
If you execute this code in a JS console, you'll see an output array
[2, 3, 5, 6].
But can you execute this code on a JS object? Do you think the following will work:
If you have ever written JS, your mind would immediately tell you that this is illegal. Why? Because the
map function is not designed to work with objects.
Programming on abstractions#
Clojure lets you run the same function,
map, on all native data structures. This is because
The examples in this chapter are coded in the
first-project.seq namespace. We are not going to ask you its disk path but assume you know it!
To get the sequence representation of a data structure, we can use the
seq function converts a data structure into sequence form, if possible. In cases where the input does not implement the
ISeq interface, an error is raised. Lists are not converted, vectors and strings are converted to lists, and maps are converted to a list of vectors.
Clojure sequence operations#
Clojure is excellent for slicing and dicing data. One of the reasons behind this is its rich set of sequence operations.
These are a subset of the operations that we may encounter.
map function takes a sequence and transforms it by applying a function to each element. Its signature is:
(map fn-to-apply & sequences). Notice how we used
& in the signature. If you recall from the chapter on
Function definitions, this means that
map is variadic. We'll get to that in a moment!
Let's try a simple map by
incrementing all numbers from 0 to 9:
inc function is a part of the Clojure core, but what if you want to do something else, say square all the numbers? Simple! You can define your own
square function and pass it to
We can make our expression terser by using an anonymous function instead of a namespaced definition:
We can even go a step further by using the shorthand syntax for anonymous functions:
When I was a beginner, my team members guided me with terse forms of almost every function I wrote. I made it a personal rule that if a function feels verbose, there will be a better way to write it.
map function behaves slightly differently for Hash-Maps. The function being applied receives a vector as an input where the first element is the key and second is the value. The arguments to the function can be destructured:
Notice the use of
_ prefix for the key argument, denoting that we don't really care about it. The use of
_ is not a rule, only a convention.
In multi-threaded environments like the JVM, we can replace
pmap, which is like
map but runs in parallel, leading to better performance.
map on multiple sequences#
map function is variadic - ie it can run on multiple sequences simultaneously:
In cases where multiple sequences are passed, the function is applied to the nth element of each sequence collectively.
In the example above, we passed two equal-sized sequences of integers from 0 to 9. The function
+ was applied to the first element of both sequences to get the first element of the resulting sequence:
Then it is applied again to the second element of both sequences, and so on. This leads to a question - what happens when sequences are of unequal length? A benefit of the REPL is that you can just evaluate code inline and check for yourself! You'll find out that the longer sequence is truncated at the end:
filter function removes elements of a sequence that don't match the defined condition. Its signature is:
(filter predicate-fn sequence). The predicate function receives arguments in the same way as the
map function, ie a single element for lists/vectors and a two-element vector for Hash-Maps:
reduce function is the OG sequence operation. Its signature is:
(reduce f init-val sequence) or
(reduce f sequence). The initial value of the accumulator is assumed to be
nil if not supplied. The input
f is a function that accepts two arguments - an accumulator and the next element:
In the simple example above, we reduce a list of integers from 0 to 9. We didn't pass an accumulator start value so it will be
In the first iteration, the function
+is applied to
nil(the accumulator's initial value) and the first element of the list (0).
(+ nil 0)is 0 so our new accumulator becomes 0.
In the next pass, the function
+is applied to the last accumulator (0) and the next element (1). The resulting value (1) becomes the next accumulator.
This process is continued until the sequence is exhausted.
All destructuring concepts we have studied so far are valid in conjunction with
reduce (and all other sequence operations). Combining concepts makes our expressions terse. For example, you can find the average karma points of a list of users like so: