Loops and Loops and Loops and…

In many languages, within your first 24 hours with the language, you learn the few looping constructs that you will ever use. Often, those constructs include while, do-while, and for. Clojure does come with a number of built-in control flow tools like while and for (though Clojure’s for is a list comprehension, much fancier than the for loop that imperative languages often use to iterate over indexed elements). More importantly, in Clojure’s case, these constructs are almost always macros. We could have written them ourselves and used them in our programs. I don’t know about you, but this gives me a tremendous feeling of power.

Let’s see how easy it can be to build these ourselves, starting with while:

control_flow/while.clj
 
(​defmacro​ ​while
 
"Repeatedly executes body while test expression is true. Presumes
 
some side-effect will cause test to become false/nil. Returns nil"
 
{:added ​"1.0"​}
 
[​test​ & body]
 
`(​loop​ []
 
(​when​ ~​test
 
~@body
 
(​recur​))))

This short macro on top of the loop special form gives us the familiar while loop that ships with Clojure! Here we take the potential danger of multiple evaluation that we saw in Macros Can Be Tricky to Get Right and use it to our advantage. Each time through the loop, we re-evaluate both the test and the body of expressions passed to while, until the test returns false or nil. As the docstring implies, we’d better have some mutable state changing at some point when using while—otherwise we’ll have an infinite loop:

control_flow/while_example.clj
 
(​def​ counter (​atom​ 0))
 
 
(​while​ (​<​ @counter 3)
 
(​println​ @counter)
 
(​swap!​ counter ​inc​))
 
; 0
 
; 1
 
; 2
 
;=> nil

If we were to write the analogous counter code using the raw loop/recur machinery, it wouldn’t quite have the same clarity as the while version:

control_flow/while_as_loop_recur.clj
 
(​def​ counter (​atom​ 0))
 
 
(​loop​ []
 
(​when​ (​<​ @counter 3)
 
(​println​ @counter)
 
(​swap!​ counter ​inc​)
 
(​recur​)))
 
; 0
 
; 1
 
; 2
 
;=> nil

Many control flow macros build on top of loop/recurwhile isn’t just an isolated example. You can think of it as a low-level operation, almost like an assembly language for Clojure control flow. Clojure doesn’t have a built-in do-while, but we could easily add one. This might be useful if we need something like while but that’s guaranteed to execute the body of the expression at least once:

control_flow/do_while.clj
 
(​defmacro​ do-while [​test​ & body]
 
`(​loop​ []
 
~@body
 
(​when​ ~​test​ (​recur​))))
 
 
(​defn​ play-game [secret]
 
(​let​ [guess (​atom​ nil)]
 
(do-while (​not​​=​ (​str​ secret) (​str​ @guess))
 
(​print​ ​"Guess the secret I'm thinking: "​)
 
(​flush​)
 
(​reset!​ guess (​read-line​)))
 
(​println​ ​"You got it!"​)))
 
 
(play-game ​"zebra"​)
 
; Guess the secret I'm thinking: lion
 
; Guess the secret I'm thinking: zebra
 
; You got it!
 
;=> nil

If we were only playing this game once, a normal while loop would be fine, since the guess starts as nil. But if we played with a crafty secret of "" (and with while instead of do-while), the player would win without having to even enter an answer. So the do-while loop solves that problem. I won’t go further and try to convince you that you should look for opportunities to use a do-while loop. The important thing here is that you can create this control flow feature yourself with just a few short lines of code.

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

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