POSTS

Guess the Number, Part 4 - looping

Currently, our Guess the Numbers game only works for 1 round: the computer makes a guess, the player answers, and the game ends. In this tutorial, we’ll keep the game going until the answer is reached, which we can accomplish using a loop.

Basically, we would like to accomplish this repetitive behavior:

I've thought of a number between 0 to 99... can you guess it?
> 45
Bigger!
> 78
Smaller!
> 50
Bigger!
> 60
Smaller!
...continue until answer is found...

As Clojure uses a concept called recursion to implement looping, let’s take a look at this concept first.

Recursion

In programming, recursion is the process of having a function call itself. Why would we want a function to do this? A recursive approach allows us to break up a problem into smaller parts that can each be solved using the same solution.

As a toy example, given the multiplication problem:

500 * 332 * 641.5 * 0.132 = ?

We can divide it into the following problems:

500 * 332 = ?
? * 641.5 = ??
?? * 0.132 = ???

where I use the ? symbol to indicate the answer found for line 1, ?? for line 2 and so on. For each line, we only have to solve the problem of multiplying two numbers together. We accomplish this in Clojure with the following code:

(defn multiply-numbers [total numbers]
  (if (empty? numbers)
    total
    (multiply-numbers
     (* total (first numbers))
     (rest numbers))))

Testing the function:

=> (multiply-numbers 1 [1 2 3 4])
24
=> (multiply-numbers 1 [500 332 641.5 0.132])
1.4056548E7

The main work is done on line 5 of the program, where we multiply the running total with the first element in the list. Then, we pass the result, along with the rest of the list, back into multiply-numbers. Every time multiply-numbers is called, we check if the numbers list is empty. If so, we return total as the answer.

What we’ve implemented so far already looks like looping: the function will multiply all numbers with a running total until all given numbers have been exhausted. However, there are limitations with this approach in Clojure.

The stack overflow problem

In most software, when a call to a function is made, the computer will store some bookkeeping information, such as how to return to the original calling function, in a storage structure known as a stack. This information is kept until the called function ends. Since the stack has a finite amount of space, can you guess what will happen when a function keeps on calling itself without returning? Let’s write a program to find out:

(defn die [total]
  (println total)
  (die (inc total)))

=> (die 0)
0
1
2
...
9693
9694
9695
StackOverflowError   clojure.walk/stringify-keys/fn--6998 (walk.clj:105)

We’ve just triggered a nasty error called a stack overflow, which occurs because the stack limit has been reached. The actual limit seems to be dependent on several factors such as the OS and program configuration, but it’s not uncommon for loops in games to reach the stack limit no matter how large. We could even reach this limit with our multiply-numbers function:

=> (multiply-numbers 1 (repeat 1000 1))
1
=> (multiply-numbers 1 (repeat 9999 1))
StackOverflowError   clojure.core/cons (core.clj:29)

The first repeat gives us a sequence containing 1000 1s, while the second repeat gives us a sequence containing 9999 1s.

loop and recur

Fortunately, Clojure gives us the loop special form to bypass this limitation. From the docs:

loop
clojure.core

    (loop bindings & body)

Evaluates the exprs in a lexical context in which the symbols in the
binding-forms are bound to their respective init-exprs or parts therein.
Acts as a recur target.

First, we’ll convert our multiply-numbers function to see how loop works:

(defn multiply-numbers [numbers]
  (loop [total 1
         numbers numbers]
    (if (empty? numbers)
      total
      (recur
       (* total (first numbers))
       (rest numbers)))))

=> (multiply-numbers [2 3 4 5])
120
=> (multiply-numbers (repeat 9999 1))
1

The parameters to loop are very similar to the let form. The first parameter lets you create bindings local to the loop form, while the body parameter can take in any number of forms to create the body of the loop. loop works hand in hand with recur, another special form which reruns the loop while replacing the loop’s local bindings with fresh arguments. In effect, recur acts like a new “function call” to the loop, passing in the running total and the remaining numbers as parameters. This also means that recur must pass the same number of arguments that were bound by loop initially.

There is one condition for the use of recur: recur must be in the tail position of the loop, meaning there should be no more forms that run after recur. This restriction allows Clojure to determine that there is no need to allocate more space on the stack.

Putting it together

Finally, we have the pieces we need to finish the first part of our game, whew! Modify -main in core.clj as follows:

(defn -main []
  (println "Think of a number between 0 to 99 and I'll try to guess it!")
  (loop [minimum 0
         maximum 99]
    (let [guess (int (/ (- maximum minimum) 2))]
      (println "My guess is:" guess)
      (println "1. Bigger")
      (println "2. Smaller")
      (println "3. That's correct!")
      (print "> ")
      (flush)
      (condp = (Integer/parseInt (read-line))
        1 (recur guess maximum)
        2 (recur minimum guess)
        3 (println "All right!!!")
        (do (println "I didn't understand your answer...")
            (recur minimum maximum))))))

This time, the game runs until the computer guesses the right answer:

Think of a number between 0 to 99 and I'll try to guess it!
My guess is... 49.
1. Bigger
2. Smaller
3. That's correct!
> 1
My guess is... 25.
1. Bigger
2. Smaller
3. That's correct!
> 1
My guess is... 37.
1. Bigger
2. Smaller
3. That's correct!
>

But wait, there’s something wrong with the computer’s guesses: they keep getting smaller although the player’s answer is larger! That’s because a bug has been present in our program since the beginning: we’ve not added the minimum guess to the calculation on line 5. This bug hasn’t been obvious until now since minimum has always been 0. The actual fix for this bug is left to you, the reader :)

Now, compare our Clojure program to the one in the previous tutorial.

line 3: We’ve shifted minimum and maximum into local bindings of the loop.

line 13, 14: After receiving the player’s input at line 12, we re-run the loop using recur while replacing either the maximum or minimum of the computer’s range of guesses.

line 15: If the player’s input was not an allowed answer, we handle it by printing an error message and recurring with the same range again, using a do special form. We use do because condp only allows 1 form in that return position, but do allows us to execute several forms while returning the last form within do.

Next time, we’ll finish off our program by allowing the player to guess the number instead.