Flow Control

Clojure has very few flow control forms. In this section, you’ll meet if, do, and loop/recur. As it turns out, this is almost all you’ll ever need. Clojure provides a library of additional forms, but they’re largely built from these primitives.

Branch with if

Clojure’s if evaluates its first argument. If the argument is logically true, it returns the result of evaluating its second argument:

 (​defn​ is-small? [number]
  (​if​ (< number 100) ​"yes"​))
 (is-small? 50)
 -> ​"yes"

If the first argument to if is logically false, it returns nil:

 (is-small? 50000)
 -> nil

If you want to define a result for the “else” part of if, add it as a third argument:

 (​defn​ is-small? [number]
  (​if​ (< number 100) ​"yes"​ ​"no"​))
 (is-small? 50000)
 -> ​"no"

The when and when-not control flow macros are built on top of if and are described in when and when-not.

Introduce Side Effects with do

Clojure’s if allows only one form for each branch. What if you want to do more than one thing on a branch? For example, you might want to log that a certain branch was chosen. do takes any number of forms, evaluates them all, and returns the last.

You can use a do to print a logging statement from within an if:

 (​defn​ is-small? [number]
  (​if​ (< number 100)
 "yes"
  (do
  (println ​"Saw a big number"​ number)
 "no"​)))

which results in:

 (is-small? 200)
 | Saw a big number 200
 -> ​"no"

This is an example of a side effect. The println doesn’t contribute to the return value of is-small? at all. Instead, it reaches into the world outside the function and actually does something.

Many programming languages mix pure functions and side effects in a completely ad hoc fashion. Not Clojure. In Clojure, side effects are explicit and unusual. do is one way to say “side effects to follow.” Since do ignores the return values of all its forms except the last, those forms must have side effects to be of any use at all.

Recur with loop/recur

The Swiss Army knife of flow control in Clojure is loop:

 (loop [bindings*] exprs*)

The loop special form works like let, establishing bindings and then evaluating exprs. The difference is that loop sets a recursion point, which can then be targeted by the recur special form:

 (recur exprs*)

recur binds new values for loop’s bindings and returns control to the top of the loop. For example, the following loop/recur returns a countdown:

 (loop [result [] x 5]
  (​if​ (zero? x)
  result
  (recur (conj result x) (dec x))))
 -> [5 4 3 2 1]

The first time through, loop binds result to an empty vector and binds x to 5. Since x is not zero, recur then rebinds the names x and result:

  • result binds to the previous result conjoined with the previous x.
  • x binds to the decrement of the previous x.

Control then returns to the top of the loop. Since x is again not zero, the loop continues, accumulating the result and decrementing x. Eventually, x reaches zero, and the if terminates the recurrence, returning result.

Instead of using a loop, you can recur back to the top of a function. This makes it simple to write a function whose entire body acts as an implicit loop:

 (​defn​ countdown [result x]
  (​if​ (zero? x)
  result
  (recur (conj result x) (dec x))))
 (countdown [] 5)
 -> [5 4 3 2 1]

recur is a powerful building block. But you may not use it very often, because many common recursions are provided by Clojure’s sequence library.

For example, countdown could also be expressed as any of these:

 (into [] (take 5 (iterate dec 5)))
 -> [5 4 3 2 1]
 
 (into [] (drop-last (reverse (range 6))))
 -> [5 4 3 2 1]
 
 (vec (reverse (rest (range 6))))
 -> [5 4 3 2 1]

Don’t expect these forms to make sense yet—just be aware that there are often alternatives to using recur directly. The sequence library functions used here are described in Using the Sequence Library. Clojure will not perform automatic tail-call optimization (TCO). However, it will optimize calls to recur. Chapter 4, Functional Programming defines TCO and explores recursion and TCO in detail.

At this point, you’ve seen quite a few language features but still no variables. Some things really do vary, and Chapter 6, State and Concurrency will show you how Clojure deals with changeable references. But most variables in traditional languages are unnecessary and downright dangerous. Let’s see how Clojure gets rid of them.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.164.34