Clojure's Quiet Achiever

The sequence abstraction is at the heart of much of Clojure's power, but receives little attention. I want to tell you what's so great about it!

Iteration and recursion are considered fundamental programming concepts and show up early in any 101 course no matter what the paradigm. The sequence abstraction almost completely throws these tools out the window. You don't need them. That's the big point I want to make here; this huge fundamental part of programming is unnecessary once you have a good sequence abstraction. I can't oversell how much complexity that removes. There must be a catch...

By my count there are 80 functions in Clojure core for manipulating sequences in some way. When you first are exposed to them the natural reaction is a feeling that the cure is worse than the disease, that something simple like a loop is now a mammoth set of functions you need to learn and understand. Loops are just one thing, surely sequences are more complex. However consider for a moment that loops do lots of different things. You have to understand every loop you come across from start to end. Sequence functions abstract this away.

A well worn example is (reduce * (range 2 10)) computes the factorial of 10. We have a sequence of number that need to be multiplied together, so we can express it like that instead of an iterative expression. Lots of languages support the heavy lifters map and reduce, so what's so special about Clojure? My premise is that Clojure has a fuller set of supporting functions which mean you almost never have to drop down to loop/recur. It also has lazy sequences and persistent data structures which mean it will be fast, and you don't need to worry about the implementation.

Another simple example... Say we have a vector of numbers
(def v [1 2 3 4 5])
And we wanted to perform some operation on the sum of each item and the next item. We might implement this imperatively like so:
for i=0; i<v.length-1; i++
operate( v[i] + v[i+1] );

So how would this look like in sequence operations?
(map operate (map + v (rest v)))

Lets break that down,
(rest v) the sequence of v without it's head: (2 3 4 5)
we are mapping the + operator to two sequences: (1 2 3 4 5) and (2 3 4 5)
which will result in a new sequence (3 5 7 9)
which we map the operation over.

You can read the statement as
"create a sequence of results of calling operate on a sequence made from adding items from a sequence of v to a sequence of v without it's head."
user=> (map inc (map + v (rest v)))
(4 6 8 10)

The parts can be individually understood. They can be combined to perform pretty much anything you need to do. As individual pieces they are simple enough to understand and make building blocks to describe all sorts of transformations. This is true of all means of abstraction and means of combination, and especially true of functional abstractions. The sequence abstraction can be extended also. Check out this interesting library which allows you to do SQL like queries on sequences.

Clojure really exploits the sequence abstraction well to remove a major source of mutation and state: iteration/recursion. Sequences are built into the language and well supported with many useful utility functions.

3 comments:

  1. The parts can be individually understood. [...] They can be combined to perform pretty much anything you need to do.

    True but it's also a trait of functional programming on the whole.

    ReplyDelete
  2. Excellent point, very true. Looking back I realize I had not really made a case for Clojure sequences at all. I've edited to that effect though I feel my argument is still hand-wavy and lacking specific details.

    I'm interested in your opinion on how well other functional languages provide sequence abstractions. All of them seem to have map/reduce and a few others like ranges... but my impression is that Clojure has a much Richer set.

    ReplyDelete
  3. I don't think Clojure has a richer set of sequence functions per se—at least when comparing to other languages that do laziness. What it has got are the persistent maps, which open up the possibilities a bit more.

    Clojure also puts an stronger emphasis on using higher-level functions, where other functional languages might use recursion and pattern-matching. I guess this is a benefit for parallism as you can easily swap out the calls to map and reduce for a version that does it in parallel (or even distributed).

    ReplyDelete