This article is Lesson 11 of Andrew's book, Learn ClojureScript
In the last chapter, we looked at ClojureScript's versions of we usually call branching control structure. However, we learned that things work a little bit different in ClojureScript compared to other languages that we may be used to - control structures are expressions rather than imperative controls. Now as we come to another fundamental topic - loops - we will learn that things are once again a bit different in ClojureScript.
In this chapter:
- Survey ClojureSctipt's various looping structures
- Learn to think in terms of sequences
- Force evaluation of loops for side effects
In imperative languages, loops are used to repeat the same instructions multiple times, usually with some small variation each time that will eventually cause the loop to exit. The classic imperative loop is a while
loop in which the computer simply executes the same instructions over and over until some condition is met:
While Loop in JavaScript
var i = 0; // <1>
while (i < 10) { // <2>
console.log("Counting: " + i);
i++; // <3>
}
- Initialize a variable that will be mutated
- Set the condition for continuing the loop
- Increment the value of
i
after every pass
Since ClojureScript emphasizes both immutability of data and expression-oriented programming - and loops are inherently both mutable and statement-oriented - one must wonder whether there is any place in ClojureScript for loops. The answer is both "yes" and "no" - there are several loop-like constructs, which we will look at momentarily, but upon closer inspection, they are abstractions for other concepts that do not involve explicit looping.
Manipulating Sequences with for
The first, and perhaps most common, expression that we will study in this chapter is for
. Although it shares a name with a certain imperative loop, it is a different animal altogether. In contrast to the iterative for
, ClojureScript's for
is centered around the idea of a sequence comprehension, in which we create a new sequence by transforming, and optionally filtering, an existing one. There are multiple ways to accomplish this task in ClojureScript, but for
is certainly a concise and idiomatic option.
In its most basic form, for
takes any number of sequences and a body, and it yields a new sequence by evaluating the body for every combination of sequence elements:
for
Dissected
(for [elem1 sequence1 ;; <1>
elem2 sequence2] ;; <2>
expr) ;; <3>
- Bind each element from
sequence1
in turn toelem1
- Do the same for
sequence2
- For every combination of elements from
sequence1
andsequence2
, evaluateexpr
with the bindings,elem1
andelem2
Using for
With a Single Sequence
Although for
supports an arbitrary number of sequences, in practice it is most commonly used with just one or two. The most common usage is - as we have already mentioned - as a sequence transformation. Say we have a list of numbers and we want to find the square of each of them. What we want is to somehow describe a process that yields a new list in which each element is the square of the corresponding element in the original list. Thankfully, this is even easier to express in code than in words:
Finding the Square of 0-9
(for [n (range 10)] ;; <1>
(* n n)) ;; <2>
;; (0 1 4 9 16 25 36 49 64 81)
- Yield a new sequence by taking the numbers 0-9
- Make each number in the new sequence the square of the original
By now we should see that when used with a single input sequence, for
describes a whole-sequence transformation. When working with ClojureScript, we should try to think whether the problem before us could be represented as a sequence transformation. If so, for
provides a no-nonsense solution. Let's look at the same problem solved iteratively and with for
. Imagine that we have a number of right triangles. We know the sides that are adjacent to the right angle, and we need to find the hypotenuse of each triangle. Fist, an iterative solution in JavaScript:
Get Hypotenuse Length Iteratively
var sides = [[4.2, 6], [4, 4], [3, 4], [5.5, 3]]; // <1>
var lengths = []; // <2>
var i;
for (i = 0; i < sides.length; i++) { // <3>
lengths.push(
Math.sqrt(
Math.pow(sides[i][0], 2) +
Math.pow(sides[i][1], 2)
)
);
}
- Model the triangle sides as a 2-dimensional array
- Declare an array to hold the resulting lengths
- Iterate over the elements in sides, pushing the calculated hypotenuse length into the
lengths
array every time
This is pretty straightforward iterative code, but it is still lower-level than we would like in ClojureScript. With loops like this, it is easy to get indices mixed up (e.g. sides[i][0]
versus sides[0][i]
) or introduce off-by-1 errors. It is easy to see that this problem is just transforming one sequence into another, so we can easily use for
:
Get Hypotenuse Length With for
(let [sides-list (list [4.2 6] [4 4] [3 4] [5.5 3])] // <1>
(for [sides sides-list] // <2>
(js/Math.sqrt (+ (js/Math.pow (first sides) 2) // <3>
(js/Math.pow (second sides) 2)))))
;; (7.323933369440222 5.656854249492381 5 6.264982043070834) // <4>
- Declare a list or pairs that each represent 2 sides of a right triangle
- Use a for expression to apply a transformation to every pair in the list
- Apply the Pythagorean Theorem to find the length of the hypotenuse
- The result is a sequence with the hypotenuse of each triangle
Quick Review:
- Given pairs of points as: [[x, y], [x, y]] coordinates, write a
for
expression that calculates the distance between the points. Hint: This is very similar to the previous example.
Using for
With Multiple Sequences
Some of the power of for
comes in its ability to combine elements from multiple sequences. When given multiple sequences, it will yield an element for every unique combination of single elements from each input sequence. This type of combination is called the Cartesian product and is an important concept in mathematical set theory. Imagine we are writing an e-commerce app, and for any given product, there are several variations: color, size, and style. We could use for
to get all of the possible product combinations:
Generating Product Variations with for
(let [colors [:magenta :chartreuse :taupe] // <1>
sizes [:sm :md :lg :xl]
styles [:budget :plain :fancy]]
(for [color colors // <2>
size sizes
style styles]
[color size style])) // <3>
;; ([:magenta :sm :plain] [:magenta :sm :regular] [:magenta :sm :fancy]
;; ... [:taupe :xl :plain] [:taupe :xl :regular] [:taupe :xl :fancy])
- Declare 3 sequences
- Take every possible combination of 1 item from each collection
- Yield a vector of each color, size, and style combination
In this example, we did not do anything with the resulting product combinations other than pack them into a vector of [color size style]
, but we could have performed any sort of transformation we wanted. Consider that to accomplish the same task using an iterative loop would have required us to write loops nested 3 levels deep!
Loop modifiers: let, when, and while
So far, we have been using only the basic form of for
in which we take every element from one or more sequences. While this works for many use cases, there are times that we want to filter the results (say, we don't want to offer fancy products in the small size). Instead of filtering the list after for generates it, we can build the filtering logic directly into the for
expression itself using the :when
modifier. Again, there are times that we want to calculate some intermediate value before yielding a result. Instead of nesting a let
expression inside body of the for
, we can use the :let
modifier. Finally, if we only want to take elements up to some cut-off point, we can use the :while
modifier. To illustrate these modifiers, we will use a somewhat contrived example:
for
Modifiers
(for [n (range 100) // <1>
:let [square (* n n)] // <2>
:when (even? n) // <3>
:while (< n 20)] // <4>
(str "n is " n " and its square is " square)) // <5>
;; ("n is 0 and its square is 0"
;; "n is 2 and its square is 4"
;; "n is 4 and its square is 16"
;; ...
;; "n is 18 and its square is 324")
- Take n from the range of 0-99
- Declare a binding for the symbol
square
for each iteration as the square of n - Only include values for which n is even
- Only continue until n reaches 20
To use any of these modifiers, we can simply append it to the list of sequence expressions. None of these modifiers are difficult to understand, so we will simply outline each of them below briefly before moving on.
-
:let creates any number of bindings within the body of the
for
. It can use any of the symbols that are defined in thefor
expression as well as any other vars in scope. The usage is identical to the regularlet
form. - :when determines for which inputs to emit a value. It is followed by a predicate expression, and it will emit a value if and only if the expression is truthy.
-
:while is like
:when
, but it short-circuits the most immediate "group" when used with multiple input sequences. That is, when placed after some sequence expression, it will skip the remaining elements in the sequence after which it is placed and continue with the combination that is formed by taking the next item in the previous sequence and the first item in the sequence after which it is placed. To illustrate this behaviour, consider how the placement of the while clause affects the behaviour of the following example:
Behaviour of :while
Modifier
Quick Review
- White a
for
expression that takes 2 values calledx
andy
both from(range 50)
and yields the pairs[x y]
for all even values ofx
and odd values ofy
when the product ofx
andy
is less than100
. - Go back to the product variations example and use
:when
to filter out all variations that are both:magenta
and:fancy
Performing Explicit Recursion with loop
and recur
In the next chapter, we will look in more detail at recursive functions - that is, functions that call themselves. But as a preview, below is a simple recursive function that uses the Euclidean algorithm for calculating the greatest common denominator of two numbers.
An Example Recursive Function
(defn gcd [x y] // <1>
(if (= y 0)
x
(gcd y (mod x y)))) // <2>
;; #'cljs.user/gcd
(gcd 90 60) // <3>
;; 30
- Define a
gcd
function using the Euclidean algorithm - The function calls itself as the last thing it does
- Test the function with inputs
90
and60
The next loop-like construct that we will look at is the dynamic duo of loop
and recur
. We use loop
/recur
in cases where we want to use a recursive process but do not need a separate, named function for it. The general form that we use with loop
is as follows:
loop
Dissected
(loop [name-1 init-value-1 // <1>
name-2 init-value-2]
body-exprs // <2>
(recur next-value-1 next-value-2)) // <3>
- Pass in any number of bindings along with their value for the first pass of the loop
- Any number of body expressions
- Optionally recur to the beginning of the loop, supplying the values for each binding during the next iteration
This is the closest construct to an imperative loop that ClojureScript has to a traditional imperative loop. 1 Translating our gcd
function from above into a loop
/recur
form is trivial:
Implementing gcd
with loop
(defn gcd-loop [a b]
(loop [x a // <1>
y b]
(if (= y 0)
x // <2>
(recur y (mod x y))))) // <3>
;; #'cljs.user/gcd-loop
(gcd-loop 90 60)
;; 30
- Initialize the loop with the function's inputs
- Return
x
wheny
is0
- Recur in the case that
y
is not0
As we see above, we can easily control when the loop exits and when it recurses by placing the call to recur
inside one branch of a conditional and another value in the other branch. This gives us the same sort of granularity that we are probably used to with imperative loops, but with less boilerplate.
recur Nuances
loop
is a very useful construct that lets us simplify many types of recursive functions. However, it cannot replace all recursive functions, only a particular class known as "tail recursive" functions. These particular functions are ones that call themselves as the very last thing they do (in the "tail" position) of the function. As we mentioned in the last chapter, every time a recursive function calls itself, it consumes a stack frame, and if it recurses too deeply, the JavaScript runtime will stop execution with an error. However, recursive processes written with loop
and recur
can recurse arbitrarily deeply because the ClojureScript compiler is able to optimize them into an imperative loop. For this reason, loop
is also usually faster than functional recursion.
Because loop
only works with tail recursion, we need to be careful that no evaluation is attempted after a recur
. Thankfully, the ClojureScript compiler warns us if the recur
is not in the tail position, and it will not compile the code until we move the recur
. Below is an example of a proper call to recur
along with an illegal one.
Legal and Illegal recur
(loop [i 0
numbers []]
(if (= i 10)
numbers
(recur (inc i) (conj numbers i)))) // <1>
;; [0 1 2 3 4 5 6 7 8 9]
(loop [i 7
fact 1]
(if (= i 1)
fact
(* i (recur (dec i) (* i fact))))) // <2>
;; ---- Could not Analyze <cljs form> line:5 column:22 ----
;;
;; Can't recur here at line 5 <cljs repl>
;;
;; 1 (loop [i 7
;; 2 fact 1]
;; 3 (if (= i 1)
;; 4 fact
;; 5 (* i (recur (dec i) (* i fact)))))
;; ^---
;;
;; ---- Analysis Error ----
;; nil
- It is legal to recur when it is the last thing to be evaluated in a loop
- Cannot recur here because we need to multiply by
i
after recurring
So we see that not every recursive function can be translated into a loop
, but if it can, there is a definite performance benefit. This has been a rather brief introduction to loop
, but we will use it quite often over the course of this book, so we will have ample opportunity to get more familiar with it.
Looping for Side Effects
Having covered for
for sequence comprehensions and loop
/recur
for explicit recursion. There is one remaining category of looping in ClojureScript: looping for side effects. Recall that a side effect is an effect that our program causes outside the pure calculations that it performs, such as adding and removing DOM nodes or sending data to a server-side API.
One of the difficulties with ClojureScript is that many of its sequence operations (including for
) are lazy. That is, they do not produce results until called on to do so. Consider this for
expression:
(for [i (range 10)]
(println i))
If you enter this into the REPL, it will print the numbers 0-9, just as we would expect. However, if we were to only request a few values from the sequence generated by the for
, we would see a surprising result:
Lazy Evaluation
(take 3 // <1>
(for [i (range 10)]
(println i)))
;; 0 // <2>
;; 1
;; 2
;; (nil nil nil) // <3>
(do (for [i (range 10)]
(println i))
(println "Done"))
;; Done
;; nil // <4>
- Only take 3 elements from the sequence given by the
for
expression -
println
gets called only 3 times - not 10 - Since
println
returnsnil
, the result is a sequence filed withnil
- No numbers are printed, since we never need the results of the
for
The for
will only evaluate when the results are required to complete some other calculation - and for only as many values in the sequence as are required. This laziness is, as we will see in a later chapter, very useful for some things, but it can cause unexpected behaviour when dealing with side effects, thus the need to force evaluation.
Evaluating an Existing Sequence With dorun
The first and simplest way to ensure that a sequence is fully evaluated (and thus all side effects that it may cause are run) is to wrap the sequence in a dorun
. When just given a sequence, it will execute the code necessary to realize every value in succession, and it returns nil. For example, we could force the numbers to print in the example above simply by wrapping the for
expression with dorun
:
Forcing Evaluation of a Lazy Sequence
(do (dorun // <1>
(for [i (range 10)]
(println i)))
(println "Done"))
;; 0 // <2>
;; 1
;; 2
;; 3
;; 4
;; 5
;; 6
;; 7
;; 8
;; 9
;; Done
;; nil
- Wrap the
for
indorun
- All numbers are printed as expected
Looping for Effects With doseq
While dorun
is often what we want if we have a sequence that performs side effects as each element is realized, we more often want to iterate over an existing sequence and perform a side effect for each element. In this case, we can use doseq
. The syntax of doseq
is identical to for
(including the modifiers), but it evaluates immediately and returns nil
instead of a sequence. If we think about sending a list of users to a back-end API, we certainly want to ensure that the code is evaluated when we expect it to, so we can use doseq
, as in the example below:
(defn send-to-api [user] // <1>
(println "Sending to API:" user))
;; #'cljs.user/send-to-api
(let [users [{:name "Alice"}
{:name "Bob"}
{:name "Carlos"}]]
(doseq [user users] // <2>
(send-to-api user))
(println "Done!"))
;; Sending to API: {:name Alice} // <3>
;; Sending to API: {:name Bob}
;; Sending to API: {:name Carlos}
;; Done!
;; nil
- Stub the
send-to-api
function - Iterate through the
users
collection - Side effects are performed immediately
Here we iterate through the users
list and call send-to-api
for each user. Since we do not care about the return value of that function, doseq
is the perfect option here.
Quick Review
- What would happen if we had used
for
in the previous example instead ofdoseq
? - There is a function similar to
dorun
calleddoall
. Look it up online and explain when you might use one versus the other. - DOM manipulation is a side effect. What are some use cases for using
doseq
in conjunction with the DOM?
Summary
We have now seen ClojureScript's core looping features. While for
and while
loops are critical in many languages, we have learned that ClojureScript does not even have these concepts. Its looping constructs center around one of three things:
- Sequence operations (
for
) - Recursion (
loop
/recur
) - Forcing evaluation of side effects (
doseq
)
Even though we may at first find it difficult to solve problems without the traditional imperative loops, we will quickly discover that a "Clojuresque" solution is often simpler. As we get more accustomed to thinking in terms of sequences and recursion, the ClojureScript way will become second nature.
-
Interestingly enough,
loop
compiles down to awhile
loop in JavaScript ↩
Top comments (0)