Functions that take functions
Passing a function to another function: map, filter, reduce and apply, plus the short spelling for a function you only need once.
A function is a value like any other
A function in Clojure is a value: you can put it in a vector, hand it to another function, and get one back. map takes a function and a sequence and answers the sequence with the function applied to each value. (map inc [1 2 3]) gives (2 3 4), with no loop written by hand and the intent right there in the line. The function can be stored in a name, built by another function, or passed along; map does not care where it came from, only that it can take one value and answer one.
The function you pass does not need a name. #(str % "!") is a function of one argument that adds an exclamation mark, and % is that argument; with two arguments the names are %1 and %2. Use the short form for one-liners, and (fn [x] ...) as soon as it grows past an expression or two.
filter and reduce
filter keeps the values the test says yes to: (filter odd? [1 2 3 4]) gives (1 3). reduce folds a sequence into one value: (reduce + [1 2 3 4]) adds them up. With a starting value it is (reduce + 10 [1 2 3]), which is what you want when the sequence might be empty. With a starting value, the first call receives that value and the first item, so an empty sequence simply answers the starting value.
Read reduce as "take the first two, combine them, then combine that with the next, then with the next". Almost every summary over data is a reduce with a small function you write yourself.
apply, and the shape of the family
apply hands a sequence to a function as its arguments: (apply + [1 2 3]) is (+ 1 2 3). It is the bridge between arithmetic, which takes its arguments separately, and the sequences you now work with. It also takes extra arguments before the sequence: (apply + 1 [2 3]) is (+ 1 2 3), so a fixed prefix and a varying tail can meet in one call.
map and filter answer lazily: they describe the work and do it when something asks for the values. Printing the result is what asks, which is why the pane always shows the answer right after the line runs.
The function decides the shape of the answer
map always answers one value per input, filter answers zero or one per input, and reduce answers one value in total. That difference is the quickest way to choose between them: if the answer should have the same length as the input, reach for map; if it should be shorter, filter; if it should be one thing, reduce. The shape of the answer is the test of whether a step belongs in the pipeline at all: a step that changes the length is a filter, one that changes every value is a map, and one that collapses is a reduce.
The function you pass has to match that shape. map’s function takes one item and returns one value; filter’s takes one item and returns a truth value; reduce’s takes the running answer and the next item and returns the next answer.
Reduce can build any kind of answer
The accumulator does not have to be a number. (reduce (fn [text word] (str text word " ")) "" words) builds a string, and the same shape can build a map, a vector, or the longest word seen so far.
Read the function as: given the answer so far and the next value, what is the answer now? Once you can answer that sentence, reduce stops being about addition. The starting value is the answer for an empty sequence, so it has to be the identity for the operation: 0 for a sum, "" for a text build, and {} for a map build.
Four functions that cover most of the work
(defn square [n]
(* n n))
(println (map square [1 2 3 4]))
(println (map #(* % %) [1 2 3]))
(println (filter odd? [1 2 3 4 5]))
(println (reduce + [1 2 3 4]))
(println (reduce + 10 [1 2 3]))
(println (reduce max [3 9 4]))
(println (apply + [1 2 3 4]))
(println (apply str ["Clojure" " " "rocks"]))Small functions, applied over and over
Run it and read the seven lines. Then write a `tax` function and use `map` to apply it to a vector of prices, and use `reduce` to add the result up.
A sequence prints with round brackets, a vector with square ones, so (map inc [1 2 3]) shows up as (2 3 4). That is the same three values; map simply answers a sequence rather than a vector.
Three habits cause surprises. map with no collection returns a transducer rather than a sequence, so (map inc) is not an error but also not the answer you expected. Side effects inside a mapped function may not run until the result is realised, so print after the sequence, not inside it. And reduce over an empty sequence with no starting value calls the function with no arguments — fine for +, an error for a function that needs two.
Try these next
- Map
countover a vector of words and print the result. - Filter a vector of numbers down to the even ones, then square them with map.
- Reduce a vector of prices with a starting value of 100 and explain the answer.
Counting and choosing words
Run it and read the four lines. Then add a word of your own and a second pair, and replace the `reduce` with `apply` to check the total is the same.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does (map inc [1 2 3]) give?
