Looping | Lesson 11

Lesson 11: Looping

In the last lesson, 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 lesson:

  • Survey ClojureScript’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:

let i = 0;                                                 // <1>

while (i < 10) {                                           // <2>
    console.log("Counting: " + i);
    i++;                                                   // <3>
}

While Loop in JavaScript

  1. Initialize a variable that will be mutated
  2. Set the condition for continuing the loop
  3. 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 lesson 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 [elem1 sequence1                                      ;; <1>
      elem2 sequence2]                                     ;; <2>
  expr)                                                    ;; <3>

for Dissected

  1. Bind each element from sequence1 in turn to elem1
  2. Do the same for sequence2
  3. For every combination of elements from sequence1 and sequence2, evaluate expr with the bindings, elem1 and elem2

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:

(for [n (range 10)]                                        ;; <1>
  (* n n))                                                 ;; <2>
;; (0 1 4 9 16 25 36 49 64 81)

Finding the Square of 0-9

  1. Yield a new sequence by taking the numbers 0-9
  2. 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:

let sides = [[4.2, 6], [4, 4], [3, 4], [5.5, 3]];          // <1>
let lengths = [];                                          // <2>
let 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)
        )
    );
}

Get Hypotenuse Length Iteratively

  1. Model the triangle sides as a 2-dimensional array
  2. Declare an array to hold the resulting lengths
  3. 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:

(let [sides-list (list [4.2 6] [4 4] [3 4] [5.5 3])]       ;; <1>
  (for [sides sides-list]                                  ;; <2>
    (Math.sqrt (+ (Math.pow (first sides) 2)               ;; <3>
                  (Math.pow (second sides) 2)))))
                                                           ;; <4>
;; (7.323933369440222 5.656854249492381 5 6.264982043070834)

Get Hypotenuse Length With for

  1. Declare a list or pairs that each represent 2 sides of a right triangle
  2. Use a for expression to apply a transformation to every pair in the list
  3. Apply the Pythagorean Theorem to find the length of the hypotenuse
  4. 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:

(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])

Generating Product Variations with for

  1. Declare 3 sequences
  2. Take every possible combination of 1 item from each collection
  3. 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 [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")

for Modifiers

  1. Take n from the range of 0-99
  2. Declare a binding for the symbol square for each iteration as the square of n
  3. Only include values for which n is even
  4. 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 the for expression as well as any other vars in scope. The usage is identical to the regular let 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 behavior, consider how the placement of the while clause affects the behavior of the following example:

Behavior of :while Modifier

Behavior of :while Modifier

Quick Review

  • White a for expression that takes 2 values called x and y both from (range 50) and yields the pairs [x y] for all even values of x and odd values of y when the product of x and y is less than 100.
  • 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 lesson, 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.

(defn gcd [x y]                                            ;; <1>
  (if (= y 0)
    x
    (gcd y (mod x y))))                                    ;; <2>
;; #'cljs.user/gcd
(gcd 90 60)                                                ;; <3>
;; 30

A Recursive Function

  1. Define a gcd function using the Euclidean algorithm
  2. The function calls itself as the last thing it does
  3. Test the function with inputs 90 and 60

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 [name-1 init-value-1                                 ;; <1>
       name-2 init-value-2]
  body-exprs                                               ;; <2>
  (recur next-value-1 next-value-2))                       ;; <3>

loop Dissected

  1. Pass in any number of bindings along with their value for the first pass of the loop
  2. Any number of body expressions
  3. 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:

(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

Implementing gcd with loop

  1. Initialize the loop with the function’s inputs
  2. Return x when y is 0
  3. Recur in the case that y is not 0

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 lesson, 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.

(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

Legal and Illegal recur

  1. It is legal to recur when it is the last thing to be evaluated in a loop
  2. 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 100)]
  (println i))

If you enter this into the REPL, it will print the numbers 0-99, 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:

(take 3                                                    ;; <1>
  (for [i (range 100)]
    (println i)))
;; 0                                                       ;; <2>
;; 1
;; ...
;; 31
;; (nil nil nil)                                           ;; <3>
(do (for [i (range 100)]
      (println i))
    (println "Done"))
;; Done
;; nil                                                     ;; <4>

Lazy Evaluation

  1. Only take 3 elements from the sequence given by the for expression
  2. println gets called 32 times - not 100
  3. Since println returns nil, the result is a sequence filed with nil
  4. 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. If some results are needed, then it will always evaluate at least as many elements as it needs to, but it may not evaluate the entire sequence2. This laziness is, as we will see in a later lesson, very useful for some things, but it can cause unexpected behavior 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:

(do (dorun                                                 ;; <1>
      (for [i (range 100)]
        (println i)))
    (println "Done"))
;; 0                                                       ;; <2>
;; 1
;; ...
;; 99
;; Done
;; nil

Forcing Evaluation of a Lazy Sequence

  1. Wrap the for in dorun
  2. 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
  1. Stub the send-to-api function
  2. Iterate through the users collection
  3. 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 of doseq?
  • There is a function similar to dorun called doall. 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 “Clojure-esque” solution is often simpler. As we get more accustomed to thinking in terms of sequences and recursion, the ClojureScript way will become second nature.


  1. Interestingly, loop compiles down to a while loop in JavaScript. ↩︎

  2. At the time of this writing, ClojureScript will evaluate lazy sequences 32 elements at a time, but this is an implementation detail, and you should never rely on it materializing the entire sequence if not necessary. ↩︎