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.

Project Source Code

Get the project source code below, and follow along with the lesson material.

Download Project Source Code

To set up the project on your local machine, please follow the directions provided in the README.md file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.

This lesson preview is part of the Tinycanva: Clojure for React Developers course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to Tinycanva: Clojure for React Developers, plus 70+ \newline books, guides and courses with the \newline Pro subscription.

Thumbnail for the \newline course Tinycanva: Clojure for React Developers
  • [00:00 - 00:08] We have been using the word sequence or Seq without a concrete example. This is because sequence is not concrete but in abstraction.

    [00:09 - 00:21] 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?

    [00:22 - 00:29] Many languages come bundled with functions like map, reduce and filter. In ES6, these functions work on arrays.

    [00:30 - 00:42] The map function, for example, takes an array and a function to apply. If the input array is 1, 2, 4, 5 and the function is an increment function, the map code would look something similar to this.

    [00:43 - 00:51] If you execute this code in a JS console, you would see an output array 2356. But can you execute this code on a JS object?

    [00:52 - 00:59] Do you think the following will work? If you have ever written JS, your mind would immediately tell that this is illegal.

    [01:00 - 01:04] Why? Because the map function is not designed to work with objects.

    [01:05 - 01:12] Closure lets you run the same function map on all native data structures. This is because map is not tied to a data type.

    [01:13 - 01:23] Instead, it is tied to the abstract concept of sequence. All sequence functions can be called on any data structures that follows the rules of a sequence.

    [01:24 - 01:31] For the sake of a mental model, think of sequences as lists. The examples in this chapter are coded in the first project or execute namespace.

    [01:32 - 01:42] We are not going to ask you its disk path and assume you know it already. To get the sequence representation of a data structure, we can use the sq function.

    [01:43 - 01:53] The sq function converts a data structure into a sequence form if it is possible. In cases where the input does not implement iSq interface and error res raised.

    [01:54 - 02:05] Lists are not converted, vectors and strings are converted to lists and maps are converted into a list of vectors. Closure is excellent for slicing and dicing data.

    [02:06 - 02:15] One of the reasons behind this is its rich set of sequence operations. All functions defined for a sequence can be called on all sequence data structures.

    [02:16 - 02:24] There is a subset of operations that we might encounter. The map function takes a sequence and transforms it by applying a function to each element.

    [02:25 - 02:31] Its signature is map, function to apply and the sequence. Notice how we used ampersand in the signature.

    [02:32 - 02:38] If you recall from the chapter on function definitions, this means that map is variadic. We will get to it in a moment.

    [02:39 - 02:46] Let's try a simple map by incrementing all numbers from 0 to 9. The ink function is a part of the closure core.

    [02:47 - 02:50] But what if you want to do something else? Say square all the numbers?

    [02:51 - 03:00] Simple, you can define your own square function and pass it to map. We can make our expression more terse using an anonymous function instead of a namespace function.

    [03:01 - 03:12] We can go a step further by using shorthand syntax for anonymous function. When i was a beginner, my team members guided me with terse forms of almost every function I wrote.

    [03:13 - 03:22] I made it a personal rule that if a function feels verbose, there is probably a better way to write it. The map function behaves slightly differently for hash maps.

    [03:23 - 03:32] The function to apply receives a vector as an input where the first element is the key and the second is the value. You can destructure it as usual.

    [03:33 - 03:37] Notice the use of underscore prefix for key argument. Denoting that we don't really care about it.

    [03:38 - 03:53] In multi-threaded environments like JVM, we can replace map with pmap which is like a map except a transient parallel leading to better performance. The map function is very e-dick, that is it can run on multiple sequences simultaneously.

    [03:54 - 04:08] In cases where multiple sequences are passed, the function is applied to nth element of each sequence collectively. In the example above, we pass two equal size sequences of integers from 0 to 9.

    [04:09 - 04:19] The function plus was applied to the first element of both sequences to get the first element of the resulting sequence. Then applied again to the second element and so on.

    [04:20 - 04:30] This leads to the question that what happens when the sequences are of an equivalent. A benefit of the repel is that you can just evaluate code inline and check for yourself.

    [04:31 - 04:40] You will find out that the longer sequence is truncated at the end. The filter function removes elements of a sequence that do not match the defined condition.

    [04:41 - 04:53] Its signature is filter predicate fn and sequence. The predicate function receives arguments same as the map function, i.e. a single element for lists and vectors and a two element vector for hash maps.

    [04:54 - 05:02] The reduce function is the og sequence operation. Its signature is reduce f, initial value and the sequence.

    [05:03 - 05:13] We can also skip the initial value and just try to reduce function to apply and sequence. The operating function accepts two arguments, an accumulator and the next element.

    [05:14 - 05:22] In the simple example above, we reduce a list of integers 0 to 9. We did not pass a default start value, so it is assumed to be null.

    [05:23 - 05:33] In the first iteration, the function plus is applied to nil, the initial value and the first element of the list. This results become a new accumulator.

    [05:34 - 05:45] 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 and the process is continued until the sequence is exhausted.

    [05:46 - 05:56] All these structuring concepts we have studied so far are valid in conjunction with reduce and all other sequence operations. Combining concepts make our expressions terse.

    [05:57 - 06:04] For example, you can find the average Karma points of a list of users like so. Want it to be more terse?

    [06:05 - 06:16] Use shorthand definition for inline function. We feel that this is the right time to point out that our aim should not be to use the minimum lines of code, specially at the cost of readability.

    [06:17 - 06:26] Closure is expressive, but it's your responsibility to use it wisely. The key function is similar to filter and has the same signature.

    [06:27 - 06:36] It returns the value of the supplied pure function if f of item is not nil. Do you remember that keywords can be used as functions to pull out values from a map?

    [06:37 - 06:49] This is a good time to point out that maps can also be used as functions with a similar effect. Combining this map as a function with keep, we can pull out a list of values from a map like so.

    [06:50 - 06:57] The permutations of things we can do with sequences keep growing. The remove function is filters inverse and has the same signature.

    [06:58 - 07:09] It returns all the values from the input sequence where predicate function returns false. Since sequences are abstract, the input data structure might not be the same as output data structure.

    [07:10 - 07:22] For example, if you have a hash map of cities and their respective temperatures and you want to figure out all cities with temperature below 30, you might use a filter . Notice how we passed in a hash map but received the list.

    [07:23 - 07:32] The into function helps you shape shift data structures. It can be used in a variety of ways but the signature we are interested in is into, to and from.

    [07:33 - 07:43] It is particularly useful with sequence operations. To get a map back from the list above, we can specify an empty map as the two argument.

    [07:44 - 07:54] Into works by conjoining every element of the sequence with the two argument. When sconge is defined for sequences, the same procedure works on vectors too.

    [07:55 - 08:02] Do you think it will work on sets? What do you think will happen if we try to shape shift temp under 30 into a set ?

    [08:03 - 08:15] The groupby function takes a function and a sequence and returns a map. The function is applied to every element of the collection and the keys of the resulting maps are the set of values returned when f was applied.

    [08:16 - 08:22] The values of the map is a vector of elements where the result matches the key. It is easier to understand with an example.

    [08:23 - 08:31] The even function was applied to list of integers 0 to 9. The set of results on the application of f to each element was true and false.

    [08:32 - 08:41] The respective elements appeared as values of the respective result. Since keywords can be used as functions with hash maps, you can group sequences of maps by keys.

    [08:42 - 08:49] In this example, we grouped all users who are the same age. Partition converts a collection into groups.

    [08:50 - 08:57] In its simplest form, its signature is partition, the step size and collection. We use the term sequence in collection interchangeably.

    [08:58 - 09:10] In this example, if number of elements in the collection is such that a complete partition cannot be created, the extra elements are truncated. Two elements are selected at a step size of 4.

    [09:11 - 09:21] We can also use a pad collection to complete partitions and prevent truncation. The signature for using pad is partition size, step, pad sequence and the sequence.

    [09:22 - 09:30] If pad sequence is defined, unequal partitions might also be returned. Sort helps with rearranging a collection on the basis of a comparator.

    [09:31 - 09:40] One of its signature is sort sequence. In its simplest form, we can just use sort with a sequence and it will return the sequence in a sending order if possible.

    [09:41 - 09:51] Sort can also accept a comparator as the second argument. The Juxed function is an honorary mention, which is a company named Juxed, help me progress in my closure journey.

    [09:52 - 09:59] The function signature is Juxed and a list of functions. It takes an arbitrary number of functions and returns a higher order function.

    [10:00 - 10:14] When this higher order function is called with some arguments, the result is a vector of all functions applied to the arguments. The function plus and star is applied to arguments 1, 2, 3, 4, 5 and the result is returned as a vector.

    [10:15 - 10:26] You can use filter map reduce functions without passing a collection like so. This creates a transducer, which is an alternate and sometimes more efficient way to work with sequences.

    [10:27 - 10:34] We will not cover transducers in this course as they take the abstraction in one level higher. We will however form a mental model at least.

    [10:35 - 10:43] If you have a sequence and want to apply multiple filters to it, you can call filter function repeatedly. This leads to repeated traversal of the sequence.

    [10:44 - 10:58] First, F1 is applied to each element of the sequence, then F2 is applied to the result of the last filter and so on. This operation can be made more efficient if we compose all filters and apply them together in one go.

    [10:59 - 11:05] This is the basic concept of a transducer. The same goes for map and reduce.

    [11:06 - 11:13] Sequence operations are lazy. Most of the time a sequence operation is not evaluated until the point where results are needed.

    [11:14 - 11:26] This makes your program more efficient and also lets you work with infinite sequences as they were finite. If we print all elements of a list but return another value, we will discover that the sequence was never realized.

    [11:27 - 11:35] If you check the console, you will find that no number was printed when you call the function. This is because the result of function is independent of the map.

    [11:36 - 11:44] This sometimes might be an issue and can be mitigated using the doall function. The doall function forces the evaluation of a lazy sequence.

    [11:45 - 11:55] You'd notice that the numbers are printed in your runtime, i.e. the notes script running in the terminal. In conclusion, sequence manipulation is one of closure's speciality.

    [11:56 - 12:05] In this chapter, we learned about important sequence operations. We used them in conjunction with concepts learned before and saw how functional ities grew together.

    [12:06 - 12:14] We also learned about the abstract and lazy nature of sequence operations. After completing this chapter, you should feel comfortable reading closure source code.

    [12:15 - 12:20] As with all chapters in this module, it will be unfair to assume that one can get this right in the first go.