This video is available to students only

Sequence Operations

Clojure sequences are abstract. In this chapter, we'll make sense of what exactly we mean by that, and learn about common operations.

Sequence 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 map, reduce, 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, map code in JavaScript might look something like:

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 map and other sequence functions are not tied to a data type, like in JavaScript. Instead, they are tied to the abstract concept of sequence. All sequence functions can be called on any data structure that follows the rules of a sequence. For the sake of a mental model, sequences are lists.

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.

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#

The 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:

The 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 map:

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.

The 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 map with pmap, which is like map but runs in parallel, leading to better performance.

map on multiple sequences#

The 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#

The 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#

The 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 nil.

  • 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:

 

This page is a preview of Tinycanva: Clojure for React Developers

Please select a discussion on the left.