4 Problems and Procedures A great discovery solves a great problem, but there is a grain of discovery in the solution of any problem. Your problem may be modest, but if it challenges your curiosity and brings into play your inventive faculties, and if you solve it by your own means, you may experience the tension and enjoy the triumph of discovery. George Pólya, How to Solve It Computers are tools for performing computations to solve problems. In this chapter, we consider what it means to solve a problem and explore some strategies for constructing procedures that solve problems. 4.1 Solving Problems Traditionally, a problem is an obstacle to overcome or some question to answer. Once the question is answered or the obstacle circumvented, the problem is solved and we can declare victory and move on to the next one. When we talk about writing programs to solve problems, though, we have a larger goal. We don’t just want to solve one instance of a problem, we want an algorithm that can solve all instances of a problem. A problem is defined by problem its inputs and the desired property of the output. Recall from Chapter 1, that a procedure is a precise description of a process and a procedure is guaranteed to always finish is called an algorithm. The name algorithm is a Latinization of the name of the Persian mathematician and scientist, Muhammad ibn Mūsā al-Khwārizmı̄, who published a book in 825 on calculation with Hindu numerals. Although the name algorithm was adopted after al-Khwārizmı̄’s book, algorithms go back much further than that. The ancient Babylonians had algorithms for finding square roots more than 3500 years ago (see Exploration 4.1). For example, we don’t just want to find the best route between New York and Washington, we want an algorithm that takes as inputs the map, start location, and end location, and outputs the best route. There are infinitely many possible inputs that each specify different instances of the problem; a general solution to the problem is a procedure that finds the best route for all possible inputs.1 To define a procedure that can solve a problem, we need to define a procedure that takes inputs describing the problem instance and produces a different information process depending on the actual values of its inputs. A procedure 1 Actually finding a general algorithm that does without needing to essentially try all possible routes is a challenging and interesting problem, for which no efficient solution is known. Finding one (or proving no fast algorithm exists) would resolve the most important open problem in computer science! 54 4.2. Composing Procedures takes zero or more inputs, and produces one output or no outputs2 , as shown in Figure 4.1. Inputs Procedure Output Figure 4.1. A procedure maps inputs to an output. Our goal in solving a problem is to devise a procedure that takes inputs that define a problem instance, and produces as output the solution to that problem instance. The procedure should be an algorithm — this means every application of the procedure must eventually finish evaluating and produce an output value. There is no magic wand for solving problems. But, most problem solving involves breaking problems you do not yet know how to solve into simpler and simpler problems until you find problems simple enough that you already know how to solve them. The creative challenge is to find the simpler subproblems that can be combined to solve the original problem. This approach of solving problems by breaking them into simpler parts is known as divide-and-conquer. divide-and-conquer The following sections describe a two key forms of divide-and-conquer problem solving: composition and recursive problem solving. We will use these same problem-solving techniques in different forms throughout this book. 4.2 Composing Procedures One way to divide a problem is to split it into steps where the output of the first step is the input to the second step, and the output of the second step is the solution to the problem. Each step can be defined by one procedure, and the two procedures can be combined to create one procedure that solves the problem. Figure 4.2 shows a composition of two functions, f and g . The output of f is used as the input to g. Input f g Output Figure 4.2. Composition. We can express this composition with the Scheme expression (g (f x)) where x is the input. The written order appears to be reversed from the picture in Figure 4.2. This is because we apply a procedure to the values of its subexpressions: 2 Although procedures can produce more than one output, we limit our discussion here to procedures that produce no more than one output. In the next chapter, we introduce ways to construct complex data, so any number of output values can be packaged into a single output. Chapter 4. Problems and Procedures 55 the values of the inner subexpressions must be computed first, and then used as the inputs to the outer applications. So, the inner subexpression (f x) is evaluated first since the evaluation rule for the outer application expression is to first evaluate all the subexpressions. To define a procedure that implements the composed procedure we make x a parameter: (define fog (lambda (x) (g (f x)))) This defines fog as a procedure that takes one input and produces as output the composition of f and g applied to the input parameter. This works for any two procedures that both take a single input parameter. For example, we can compose the square and cube procedures from Chapter 3: (define sixth-power (lambda (x) (cube (square x)))) Then, (sixth-power 2) evaluates to 64. 4.2.1 Procedures as Inputs and Outputs All the procedure inputs and outputs we have seen so far have been numbers. The subexpressions of an application can be any expression including a procedure. A higher-order procedure is a procedure that takes other procedures as in- higher-order puts or that produces a procedure as its output. Higher-order procedures give us procedure the ability to write procedures that behave differently based on the procedures that are passed in as inputs. For example, we can create a generic composition procedure by making f and g parameters: (define fog (lambda (f g x) (g (f x)))) The fog procedure takes three parameters. The first two are both procedures that take one input. The third parameter is a value that can be the input to the first procedure. For example, > (fog square cube 2) evaluates to 64, and > (fog (lambda (x) (+ x 1)) square 2) evaluates to 9. In the second example the first parameter is the procedure produced by the lambda expression (lambda (x) (+ x 1)). This procedure takes a number as input and produces as output that number plus one. We use a definition to name this procedure inc (short for increment): (define inc (lambda (x) (+ x 1))) A more useful composition procedure would separate the input value, x, from the composition. The fcompose procedure takes two procedures as inputs and produces as output a procedure that is their composition:3 (define fcompose (lambda (f g ) (lambda (x) (g (f x))))) The body of the fcompose procedure is a lambda expression that makes a procedure. Hence, the result of applying fcompose to two procedures is not a simple 3 We name our composition procedure fcompose to avoid collision with the built-in compose procedure that behaves similarly. 56 4.3. Recursive Problem Solving value, but a procedure. The resulting procedure can then be applied to a value. Here are some examples using fcompose: > (fcompose inc inc) #<procedure> > ((fcompose inc inc) 1) 3 > ((fcompose inc square) 2) 9 > ((fcompose square inc) 2) 5 Exercise 4.1. For each expression, give the value to which the expression evaluates. Assume fcompose and inc are defined as above. a. ((fcompose square square) 3) b. (fcompose (lambda (x) (∗ x 2)) (lambda (x) (/ x 2))) c. ((fcompose (lambda (x) (∗ x 2)) (lambda (x) (/ x 2))) 1120) d. ((fcompose (fcompose inc inc) inc) 2) Exercise 4.2. Suppose we define self-compose as a procedure that composes a procedure with itself: (define (self-compose f ) (fcompose f f )) Explain how (((fcompose self-compose self-compose) inc) 1) is evaluated. Exercise 4.3. Define a procedure fcompose3 that takes three procedures as input, and produces as output a procedure that is the composition of the three input procedures. For example, ((fcompose3 abs inc square) −5) should evaluate to 36. Define fcompose3 two different ways: once without using fcompose, and once using fcompose. Exercise 4.4. The fcompose procedure only works when both input procedures take one input. Define a f2compose procedure that composes two procedures where the first procedure takes two inputs, and the second procedure takes one input. For example, ((f2compose + abs) 3 −5) should evaluate to 2. 4.3 Recursive Problem Solving In the previous section, we used functional composition to break a problem into two procedures that can be composed to produce the desired output. A particularly useful variation on this is when we can break a problem into a smaller version of the original problem. The goal is to be able to feed the output of one application of the procedure back into the same procedure as its input for the next application, as shown in Figure 4.3. Here’s a corresponding Scheme procedure: 57 Chapter 4. Problems and Procedures f Input Figure 4.3. Circular Composition. (define f (lambda (n) (f n))) Of course, this doesn’t work very well!4 Every application of f results in another application of f to evaluate. This never stops — no output is ever produced and the interpreter will keep evaluating applications of f until it is stopped or runs out of memory. We need a way to make progress and eventually stop, instead of going around in circles. To make progress, each subsequent application should have a smaller input. Then, the applications stop when the input to the procedure is simple enough that the output is already known. The stopping condition is called the base case, similarly to the grammar rules in Section 2.4. In our grammar ex- base case amples, the base case involved replacing the nonterminal with nothing (e.g., MoreDigits ::⇒ e) or with a terminal (e.g., Noun ::⇒ Alice). In recursive procedures, the base case will provide a solution for some input for which the problem is so simple we already know the answer. When the input is a number, this is often (but not necessarily) when the input is 0 or 1. To define a recursive procedure, we use an if expression to test if the input matches the base case input. If it does, the consequent expression is the known answer for the base case. Otherwise, the recursive case applies the procedure again but with a smaller input. That application needs to make progress towards reaching the base case. This means, the input has to change in a way that gets closer to the base case input. If the base case is for 0, and the original input is a positive number, one way to get closer to the base case input is to subtract 1 from the input value with each recursive application. This evaluation spiral is depicted in Figure 4.4. With each subsequent recursive call, the input gets smaller, eventually reaching the base case. For the base case application, a result is returned to the previous application. This is passed back up the spiral to produce the final output. Keeping track of where we are in a recursive evaluation is similar to keeping track of the subnetworks in an RTN traversal. The evaluator needs to keep track of where to return after each recursive evaluation completes, similarly to how we needed to keep track of the stack of subnetworks to know how to proceed in an RTN traversal. Here is the corresponding procedure: (define g (lambda (n) (if (= n 0) 1 (g (− n 1))))) Unlike the earlier circular f procedure, if we apply g to any non-negative integer 4 Curious readers should try entering this definition into a Scheme interpreter and evaluating (f 0). If you get tired of waiting for an output, in DrRacket you can click the Stop button in the upper right corner to interrupt the evaluation. 58 4.3. Recursive Problem Solving 2 1 0 1 1 1 Figure 4.4. Recursive Composition. it will eventually produce an output. For example, consider evaluating (g 2). When we evaluate the first application, the value of the parameter n is 2, so the predicate expression (= n 0) evaluates to false and the value of the procedure body is the value of the alternate expression, (g (− n 1)). The subexpression, (− n 1) evaluates to 1, so the result is the result of applying g to 1. As with the previous application, this leads to the application, (g (− n 1)), but this time the value of n is 1, so (− n 1) evaluates to 0. The next application leads to the application, (g 0). This time, the predicate expression evaluates to true and we have reached the base case. The consequent expression is just 1, so no further applications of g are performed and this is the result of the application (g 0). This is returned as the result of the (g 1) application in the previous recursive call, and then as the output of the original (g 2) application. We can think of the recursive evaluation as winding until the base case is reached, and then unwinding the outputs back to the original application. For this procedure, the output is not very interesting: no matter what positive number we apply g to, the eventual result is 1. To solve interesting problems with recursive procedures, we need to accumulate results as the recursive applications wind or unwind. Examples 4.1 and 4.2 illustrate recursive procedures that accumulate the result during the unwinding process. Example 4.3 illustrates a recursive procedure that accumulates the result during the winding process. Example 4.1: Factorial How many different arrangements are there of a deck of 52 playing cards? The top card in the deck can be any of the 52 cards, so there are 52 possible choices for the top card. The second card can be any of the cards except for the card that is the top card, so there are 51 possible choices for the second card. The third card can be any of the 50 remaining cards, and so on, until the last card for which there is only one choice remaining. 52 ∗ 51 ∗ 50 ∗ · · · ∗ 2 ∗ 1 factorial This is known as the factorial function (denoted in mathematics using the exclamation point, e.g., 52!). It can be defined recursively: 0! = 1 n! = n ∗ (n − 1)! for all n > 0 The mathematical definition of factorial is recursive, so it is natural that we can define a recursive procedure that computes factorials: Chapter 4. Problems and Procedures 59 (define (factorial n) (if (= n 0) 1 (∗ n (factorial (− n 1))))) Evaluating (factorial 52) produces the number of arrangements of a 52-card deck: a sixty-eight digit number starting with an 8. The factorial procedure has structure very similar to our earlier definition of the useless recursive g procedure. The only difference is the alternative expression for the if expression: in g we used (g (− n 1)); in factorial we added the outer application of ∗: (∗ n (factorial (− n 1))). Instead of just evaluating to the result of the recursive application, we are now combining the output of the recursive evaluation with the input n using a multiplication application. Exercise 4.5. How many different ways are there of choosing an unordered 5card hand from a 52-card deck? This is an instance of the “n choose k” problem (also known as the binomial coefficient): how many different ways are there to choose a set of k items from n items. There are n ways to choose the first item, n − 1 ways to choose the second, . . ., and n − k + 1 ways to choose the kth item. But, since the order does not matter, some of these ways are equivalent. The number of possible ways to order the k items is k!, so we can compute the number of ways to choose k items from a set of n items as: n ∗ ( n − 1) ∗ · · · ∗ ( n − k + 1) n! = k! (n − k)!k! a. Define a procedure choose that takes two inputs, n (the size of the item set) and k (the number of items to choose), and outputs the number of possible ways to choose k items from n. b. Compute the number of possible 5-card hands that can be dealt from a 52card deck. c. [?] Compute the likelihood of being dealt a flush (5 cards all of the same suit). In a standard 52-card deck, there are 13 cards of each of the four suits. Hint: divide the number of possible flush hands by the number of possible hands. Exercise 4.6. Reputedly, when Karl Gauss was in elementary school his teacher assigned the class the task of summing the integers from 1 to 100 (e.g., 1 + 2 + 3 + · · · + 100) to keep them busy. Being the (future) “Prince of Mathematics”, Gauss developed the formula for calculating this sum, that is now known as the Gauss sum. Had he been a computer scientist, however, and had access to a Scheme interpreter in the late 1700s, he might have instead defined a recursive procedure to solve the problem. Define a recursive procedure, gauss-sum, that takes a number n as its input parameter, and evaluates to the sum of the integers from 1 to n as its output. For example, (gauss-sum 100) should evaluate to 5050. Karl Gauss 60 4.3. Recursive Problem Solving Exercise 4.7. [?] Define a higher-order procedure, accumulate, that can be used to make both gauss-sum (from Exercise 4.6) and factorial. The accumulate procedure should take as its input the function used for accumulation (e.g., ∗ for factorial, + for gauss-sum). With your accumulate procedure, ((accumulate +) 100) should evaluate to 5050 and ((accumulate ∗) 3) should evaluate to 6. We assume the result of the base case is 1 (although a more general procedure could take that as a parameter). Hint: since your procedure should produce a procedure as its output, it could start like this: (define (accumulate f ) (lambda (n) (if (= n 1) 1 ... Example 4.2: Find Maximum Consider the problem of defining a procedure that takes as its input a procedure, a low value, and a high value, and outputs the maximum value the input procedure produces when applied to an integer value between the low value and high value input. We name the inputs f , low, and high. To find the maximum, the find-maximum procedure should evaluate the input procedure f at every integer value between the low and high, and output the greatest value found. Here are a few examples: > (find-maximum (lambda (x) x) 1 20) 20 > (find-maximum (lambda (x) (− 10 x)) 1 20) 9 > (find-maximum (lambda (x) (∗ x (− 10 x))) 1 20) 25 To define the procedure, think about how to combine results from simpler problems to find the result. For the base case, we need a case so simple we already know the answer. Consider the case when low and high are equal. Then, there is only one value to use, and we know the value of the maximum is (f low). So, the base case is (if (= low high) (f low) . . . ). How do we make progress towards the base case? Suppose the value of high is equal to the value of low plus 1. Then, the maximum value is either the value of (f low) or the value of (f (+ low 1)). We could select it using the bigger procedure (from Example 3.3): (bigger (f low) (f (+ low 1))). We can extend this to the case where high is equal to low plus 2: (bigger (f low) (bigger (f (+ low 1)) (f (+ low 2)))) The second operand for the outer bigger evaluation is the maximum value of the input procedure between the low value plus one and the high value input. If we name the procedure we are defining find-maximum, then this second operand is the result of (find-maximum f (+ low 1) high). This works whether high is equal to (+ low 1), or (+ low 2), or any other value greater than high. Putting things together, we have our recursive definition of find-maximum: Chapter 4. Problems and Procedures 61 (define (find-maximum f low high) (if (= low high) (f low) (bigger (f low) (find-maximum f (+ low 1) high)))) Exercise 4.8. To find the maximum of a function that takes a real number as its input, we need to evaluate at all numbers in the range, not just the integers. There are infinitely many numbers between any two numbers, however, so this is impossible. We can approximate this, however, by evaluating the function at many numbers in the range. Define a procedure find-maximum-epsilon that takes as input a function f , a low range value low, a high range value high, and an increment epsilon, and produces as output the maximum value of f in the range between low and high at interval epsilon. As the value of epsilon decreases, find-maximum-epsilon should evaluate to a value that approaches the actual maximum value. For example, (find-maximum-epsilon (lambda (x) (∗ x (− 5.5 x))) 1 10 1) evaluates to 7.5. And, (find-maximum-epsilon (lambda (x) (∗ x (− 5.5 x))) 1 10 0.01) evaluates to 7.5625. Exercise 4.9. [?] The find-maximum procedure we defined evaluates to the maximum value of the input function in the range, but does not provide the input value that produces that maximum output value. Define a procedure that finds the input in the range that produces the maximum output value. For example, (find-maximum-input inc 1 10) should evaluate to 10 and (findmaximum-input (lambda (x) (∗ x (− 5.5 x))) 1 10) should evaluate to 3. Exercise 4.10. [?] Define a find-area procedure that takes as input a function f , a low range value low, a high range value high, and an increment epsilon, and produces as output an estimate for the area under the curve produced by the function f between low and high using the epsilon value to determine how many regions to evaluate. Example 4.3: Euclid’s Algorithm In Book 7 of the Elements, Euclid describes an algorithm for finding the greatest common divisor of two non-zero integers. The greatest common divisor is the greatest integer that divides both of the input numbers without leaving any remainder. For example, the greatest common divisor of 150 and 200 is 50 since (/ 150 50) evaluates to 3 and (/ 200 50) evaluates to 4, and there is no number greater than 50 that can evenly divide both 150 and 200. The modulo primitive procedure takes two integers as its inputs and evaluates to the remainder when the first input is divided by the second input. For example, (modulo 6 3) evaluates to 0 and (modulo 7 3) evaluates to 1. 62 4.3. Recursive Problem Solving Euclid’s algorithm stems from two properties of integers: 1. If (modulo a b) evaluates to 0 then b is the greatest common divisor of a and b. 2. If (modulo a b) evaluates to a non-zero integer r, the greatest common divisor of a and b is the greatest common divisor of b and r. We can define a recursive procedure for finding the greatest common divisor closely following Euclid’s algorithm5 : (define (gcd-euclid a b) (if (= (modulo a b) 0) b (gcd-euclid b (modulo a b)))) The structure of the definition is similar to the factorial definition: the procedure body is an if expression and the predicate tests for the base case. For the gcd-euclid procedure, the base case corresponds to the first property above. It occurs when b divides a evenly, and the consequent expression is b. The alternate expression, (gcd-euclid b (modulo a b)), is the recursive application. The gcd-euclid procedure differs from the factorial definition in that there is no outer application expression in the recursive call. We do not need to combine the result of the recursive application with some other value as was done in the factorial definition, the result of the recursive application is the final result. Unlike the factorial and find-maximum examples, the gcd-euclid procedure produces the result in the base case, and no further computation is necessary to produce the final result. When no further evaluation is necessary to get from the result of the recursive application to the final result, a recursive definition is tail recursive said to be tail recursive. Tail recursive procedures have the advantage that they can be evaluated without needing to keep track of the stack of previous recursive calls. Since the final call produces the final result, there is no need for the interpreter to unwind the recursive calls to produce the answer. Exercise 4.11. Show the structure of the gcd-euclid applications in evaluating (gcd-euclid 6 9). Exercise 4.12. [?] Provide a convincing argument why the evaluation of (gcdeuclid a b) will always finish when the inputs are both positive integers. Exercise 4.13. Provide an alternate definition of factorial that is tail recursive. To be tail recursive, the expression containing the recursive application cannot be part of another application expression. (Hint: define a factorial-helper procedure that takes an extra parameter, and then define factorial as (define (factorial n) (factorial-helper n 1)).) Exercise 4.14. Provide a tail recursive definition of find-maximum. Exercise 4.15. [??] Provide a convincing argument why it is possible to transform any recursive procedure into an equivalent procedure that is tail recursive. 5 DrRacket provides a built-in procedure gcd that computes the greatest common divisor. We name our procedure gcd-euclid to avoid a clash with the build-in procedure. 63 Chapter 4. Problems and Procedures Exploration 4.1: Square Roots One of the earliest known algorithms is a method for computing square roots. It is known as Heron’s method after the Greek mathematician Heron of Alexandria who lived in the first century AD who described the method, although it was also known to the Babylonians many centuries earlier. Isaac Newton developed a more general method for estimating functions based on their derivatives known as Netwon’s method, of which Heron’s method is a specialization. Square root is a mathematical function that take a number, a, as input and outputs a value x such that x2 = a. For many numbers (including 2), the square root is irrational, so the best we can hope for with is a good approximation. We define a procedure find-sqrt that takes the target number as input and outputs an approximation for its square root. Heron’s method works by starting with an arbitrary guess, g0 . Then, with each iteration, compute a new guess (gn is the nth guess) that is a function of the previous guess (gn−1 ) and the target number (a): gn = g n −1 + a gn −1 2 As n increases gn gets closer and closer to the square root of a. The definition is recursive since we compute gn as a function of gn−1 , so we can define a recursive procedure that computes Heron’s method. First, we define a procedure for computing the next guess from the previous guess and the target: (define (heron-next-guess a g ) (/ (+ g (/ a g )) 2)) Next, we define a recursive procedure to compute the nth guess using Heron’s method. It takes three inputs: the target number, a, the number of guesses to make, n, and the value of the first guess, g. (define (heron-method a n g ) (if (= n 0) g (heron-method a (− n 1) (heron-next-guess a g )))) To start, we need a value for the first guess. The choice doesn’t really matter — the method works with any starting guess (but will reach a closer estimate quicker if the starting guess is good). We will use 1 as our starting guess. So, we can define a find-sqrt procedure that takes two inputs, the target number and the number of guesses to make, and outputs an approximation of the square root of the target number. (define (find-sqrt a guesses) (heron-method a guesses 1)) Heron’s method converges to a good estimate very quickly: > (square (find-sqrt 2 0)) 1 > (square (find-sqrt 2 1)) 2 1/4 Heron of Alexandria 64 4.3. Recursive Problem Solving > (square (find-sqrt 2 2)) 2 1/144 > (square (find-sqrt 2 3)) 2 1/166464 > (square (find-sqrt 2 4)) 2 1/221682772224 > (square (find-sqrt 2 5)) 2 1/393146012008229658338304 > (exact->inexact (find-sqrt 2 5)) 1.4142135623730951 The actual square root of 2 is 1.414213562373095048 . . ., so our estimate is correct to 16 digits after only five guesses. Users of square roots don’t really care about the method used to find the square root (or how many guesses are used). Instead, what is important to a square root user is how close the estimate is to the actual value. Can we change our find-sqrt procedure so that instead of taking the number of guesses to make as its second input it takes a minimum tolerance value? Since we don’t know the actual square root value (otherwise, of course, we could just return that), we need to measure tolerance as how close the square of the approximation is to the target number. Hence, we can stop when the square of the guess is close enough to the target value. (define (close-enough? a tolerance g ) (<= (abs (− a (square g ))) tolerance)) The stopping condition for the recursive definition is now when the guess is close enough. Otherwise, our definitions are the same as before. (define (heron-method-tolerance a tolerance g ) (if (close-enough? a tolerance g ) g (heron-method-tolerance a tolerance (heron-next-guess a g )))) (define (find-sqrt-approx a tolerance) (heron-method-tolerance a tolerance 1)) Note that the value passed in as tolerance does not change with each recursive call. We are making the problem smaller by making each successive guess closer to the required answer. Here are some example interactions with find-sqrt-approx: > (exact->inexact (square (find-sqrt-approx 2 0.01))) 2.0069444444444446 > (exact->inexact (square (find-sqrt-approx 2 0.0000001))) 2.000000000004511 a. How accurate is the built-in sqrt procedure? b. Can you produce more accurate square roots than the built-in sqrt procedure? c. Why doesn’t the built-in procedure do better? 65 Chapter 4. Problems and Procedures 4.4 Evaluating Recursive Applications Evaluating an application of a recursive procedure follows the evaluation rules just like any other expression evaluation. It may be confusing, however, to see that this works because of the apparent circularity of the procedure definition. Here, we show in detail the evaluation steps for evaluating (factorial 2). The evaluation and application rules refer to the rules summary in Section 3.8. We first show the complete evaluation following the substitution model evaluation rules in full gory detail, and later review a subset showing the most revealing steps. Stepping through even a fairly simple evaluation using the evaluation rules is quite tedious, and not something humans should do very often (that’s why we have computers!) but instructive to do once to understand exactly how an expression is evaluated. The evaluation rule for an application expression does not specify the order in which the subexpressions are evaluated. A Scheme interpreter is free to evaluate them in any order. Here, we choose to evaluate the subexpressions in the order that is most readable. The value produced by an evaluation does not depend on the order in which the subexpressions are evaluated.6 In the evaluation steps, we use typewriter font for uninterpreted Scheme expressions and sans-serif font to show values. So, 2 represents the Scheme expression that evaluates to the number 2. 1 2 3 4 5 6 7 8 9 Evaluation Rule 3(a): Application subexpressions (factorial 2) Evaluation Rule 2: Name (factorial 2) ((lambda (n) (if (= n 0) 1 (* n (factorial (- n 1))))) 2) Evaluation Rule 4: Lambda Evaluation Rule 1: Primitive ((lambda (n) (if (= n 0) 1 (* n (factorial (- n 1))))) 2) ((lambda (n) (if (= n 0) 1 (* n (factorial (- n 1))))) 2) Evaluation Rule 3(b): Application, Application Rule 2 Evaluation Rule 5(a): If predicate (if (= 2 0) 1 (* 2 (factorial (- 2 1)))) (if (= 2 0) 1 (* 2 (factorial (- 2 1)))) Evaluation Rule 3(a): Application subexpressions Evaluation Rule 1: Primitive (if (= 2 0) 1 (* 2 (factorial (- 2 1)))) (if (= 2 0) 1 (* 2 (factorial (- 2 1)))) Evaluation Rule 3(b): Application, Application Rule 1 10 11 12 13 14 15 16 17 18 (if false 1 (* 2 (factorial (- 2 1)))) Evaluation Rule 5(b): If alternate (* 2 (factorial (- 2 1))) Evaluation Rule 3(a): Application subexpressions (* 2 (factorial (- 2 1))) Evaluation Rule 1: Primitive (* 2 (factorial (- 2 1))) Evaluation Rule 3(a): Application subexpressions (* 2 (factorial (- 2 1))) Evaluation Rule 3(a): Application subexpressions (* 2 (factorial (- 2 1))) Evaluation Rule 1: Primitive (* 2 (factorial (- 2 1))) Evaluation Rule 3(b): Application, Application Rule 1 (* 2 (factorial 1)) Continue Evaluation Rule 3(a); Evaluation Rule 2: Name (* 2 ((lambda (n) (if (= n 0) 1 (* n (factorial (- n 1))))) 1)) Evaluation Rule 4: Lambda 19 (* 2 ((lambda (n) (if (= n 0) 1 (* n (factorial (- n 1))))) 1)) 20 (* 2 (if (= 1 0) 1 (* 1 (factorial (- 1 1))))) Evaluation Rule 3(b): Application, Application Rule 2 Evaluation Rule 5(a): If predicate 6 This is only true for the subset of Scheme we have defined so far. Once we introduce side effects and mutation, it is no longer the case, and expressions can produce different results depending on the order in which they are evaluated. 66 4.4. Evaluating Recursive Applications 21 (* 2 (if (= 1 0) 1 (* 1 (factorial (- 1 1))))) 22 (* 2 (if (= 1 0) 1 (* 1 (factorial (- 1 1))))) 23 (* 2 (if (= 1 0) 1 (* 1 (factorial (- 1 1))))) 24 (* 2 (if false 1 (* 1 (factorial (- 1 1))))) Evaluation Rule 3(a): Application subexpressions Evaluation Rule 1: Primitives Evaluation Rule 3(b): Application Rule 1 (* (* (* (* (* (* 25 26 27 28 29 30 2 2 2 2 2 2 (* (* (* (* (* (* 1 1 1 1 1 1 (factorial (factorial (factorial (factorial (factorial (factorial (- 1 1)))) (- 1 1)))) (- 1 1)))) (- 1 1)))) (- 1 1)))) (- 1 1)))) Evaluation Rule 5(b): If alternate Evaluation Rule 3(a): Application Evaluation Rule 1: Primitives Evaluation Rule 3(a): Application Evaluation Rule 3(a): Application Evaluation Rule 1: Primitives Evaluation Rule 3(b): Application, Application Rule 1 32 (* 2 (* 1 (factorial 0))) Evaluation Rule 2: Name (* 2 (* 1 ((lambda (n) (if (= n 0) 1 (* n (fact... )))) 0))) 33 (* 2 (* 1 ((lambda (n) (if (= n 0) 1 (* n (factorial (- n 1))))) 0))) 34 (* 2 (* 1 (if (= 0 0) 1 (* 0 (factorial (- 0 1)))))) 35 (* 2 (* 1 (if (= 0 0) 1 (* 0 (factorial (- 0 1)))))) 36 (* 2 (* 1 (if (= 0 0) 1 (* 0 (factorial (- 0 1)))))) 37 (* 2 (* 1 (if (= 0 0) 1 (* 0 (factorial (- 0 1)))))) 38 (* 2 (* 1 (if true 1 (* 0 (factorial (- 0 1)))))) 31 Evaluation Rule 4, Lambda Evaluation Rule 3(b), Application Rule 2 Evaluation Rule 5(a): If predicate Evaluation Rule 3(a): Application subexpressions Evaluation Rule 1: Primitives Evaluation Rule 3(b): Application, Application Rule 1 (* 2 (* 1 1)) (* 2 (* 1 1)) (* 2 1) 39 40 41 42 2 Evaluation Rule 5(b): If consequent Evaluation Rule 1: Primitives Evaluation Rule 3(b): Application, Application Rule 1 Evaluation Rule 3(b): Application, Application Rule 1 Evaluation finished, no unevaluated expressions remain. The key to evaluating recursive procedure applications is if special evaluation rule. If the if expression were evaluated like a regular application all subexpressions would be evaluated, and the alternative expression containing the recursive call would never finish evaluating! Since the evaluation rule for if evaluates the predicate expression first and does not evaluate the alternative expression when the predicate expression is true, the circularity in the definition ends when the predicate expression evaluates to true. This is the base case. In the example, this is the base case where (= n 0) evaluates to true and instead of producing another recursive call it evaluates to 1. The Evaluation Stack. The structure of the evaluation is clearer from just the most revealing steps: 1 17 31 40 41 42 (factorial 2) (* 2 (factorial 1)) (* 2 (* 1 (factorial 0))) (* 2 (* 1 1)) (* 2 1) 2 Step 1 starts evaluating (factorial 2). The result is found in Step 42. To eval- Chapter 4. Problems and Procedures 67 uate (factorial 2), we follow the evaluation rules, eventually reaching the body expression of the if expression in the factorial definition in Step 17. Evaluating this expression requires evaluating the (factorial 1) subexpression. At Step 17, the first evaluation is in progress, but to complete it we need the value resulting from the second recursive application. Evaluating the second application results in the body expression, (∗ 1 (factorial 0)), shown for Step 31. At this point, the evaluation of (factorial 2) is stuck in Evaluation Rule 3, waiting for the value of (factorial 1) subexpression. The evaluation of the (factorial 1) application leads to the (factorial 0) subexpression, which must be evaluated before the (factorial 1) evaluation can complete. In Step 40, the (factorial 0) subexpression evaluation has completed and produced the value 1. Now, the (factorial 1) evaluation can complete, producing 1 as shown in Step 41. Once the (factorial 1) evaluation completes, all the subexpressions needed to evaluate the expression in Step 17 are now evaluated, and the evaluation completes in Step 42. Each recursive application can be tracked using a stack, similarly to processing RTN subnetworks (Section 2.3). A stack has the property that the first item pushed on the stack will be the last item removed—all the items pushed on top of this one must be removed before this item can be removed. For application evaluations, the elements on the stack are expressions to evaluate. To finish evaluating the first expression, all of its component subexpressions must be evaluated. Hence, the first application evaluation started is the last one to finish. Exercise 4.16. This exercise tests your understanding of the (factorial 2) evaluation. a. In step 5, the second part of the application evaluation rule, Rule 3(b), is used. In which step does this evaluation rule complete? b. In step 11, the first part of the application evaluation rule, Rule 3(a), is used. In which step is the following use of Rule 3(b) started? c. In step 25, the first part of the application evaluation rule, Rule 3(a), is used. In which step is the following use of Rule 3(b) started? d. To evaluate (factorial 3), how many times would Evaluation Rule 2 be used to evaluate the name factorial? e. [?] To evaluate (factorial n) for any positive integer n, how many times would Evaluation Rule 2 be used to evaluate the name factorial? Exercise 4.17. For which input values n will an evaluation of (factorial n) eventually reach a value? For values where the evaluation is guaranteed to finish, make a convincing argument why it must finish. For values where the evaluation would not finish, explain why. 4.5 Developing Complex Programs To develop and use more complex procedures it will be useful to learn some helpful techniques for understanding what is going on when procedures are evaluated. It is very rare for a first version of a program to be completely correct, even for an expert programmer. Wise programmers build programs incrementally, by writing and testing small components one at a time. 68 debugging 4.5. Developing Complex Programs The process of fixing broken programs is known as debugging . The key to debugging effectively is to be systematic and thoughtful. It is a good idea to take notes to keep track of what you have learned and what you have tried. Thoughtless debugging can be very frustrating, and is unlikely to lead to a correct program. A good strategy for debugging is to: 1. Ensure you understand the intended behavior of your procedure. Think of a few representative inputs, and what the expected output should be. 2. Do experiments to observe the actual behavior of your procedure. Try your program on simple inputs first. What is the relationship between the actual outputs and the desired outputs? Does it work correctly for some inputs but not others? 3. Make changes to your procedure and retest it. If you are not sure what to do, make changes in small steps and carefully observe the impact of each change. First actual bug From Grace Hopper’s notebook, 1947 For more complex programs, follow this strategy at the level of sub-components. For example, you can try debugging at the level of one expression before trying the whole procedure. Break your program into several procedures so you can test and debug each procedure independently. The smaller the unit you test at one time, the easier it is to understand and fix problems. DrRacket provides many useful and powerful features to aid debugging, but the most important tool for debugging is using your brain to think carefully about what your program should be doing and how its observed behavior differs from the desired behavior. Next, we describe two simple ways to observe program behavior. 4.5.1 Printing One useful procedure built-in to DrRacket is the display procedure. It takes one input, and produces no output. Instead of producing an output, it prints out the value of the input (it will appear in purple in the Interactions window). We can use display to observe what a procedure is doing as it is evaluated. For example, if we add a (display n) expression at the beginning of our factorial procedure we can see all the intermediate calls. To make each printed value appear on a separate line, we use the newline procedure. The newline procedure prints a new line; it takes no inputs and produces no output. (define (factorial n) (display "Enter factorial: ") (display n) (newline) (if (= n 0) 1 (∗ n (factorial (− n 1))))) Evaluating (factorial 2) produces: Enter factorial: 2 Enter factorial: 1 Enter factorial: 0 2 The built-in printf procedure makes it easier to print out many values at once. It takes one or more inputs. The first input is a string (a sequence of characters enclosed in double quotes). The string can include special ~a markers that print Chapter 4. Problems and Procedures 69 out values of objects inside the string. Each ~a marker is matched with a corresponding input, and the value of that input is printed in place of the ~a in the string. Another special marker, ~n, prints out a new line inside the string. Using printf , we can define our factorial procedure with printing as: (define (factorial n) (printf "Enter factorial: ˜a˜n" n) (if (= n 0) 1 (∗ n (factorial (− n 1))))) The display, printf , and newline procedures do not produce output values. Instead, they are applied to produce side effects. A side effect is something that side effects changes the state of a computation. In this case, the side effect is printing in the Interactions window. Side effects make reasoning about what programs do much more complicated since the order in which events happen now matters. We will mostly avoid using procedures with side effects until Chapter 9, but printing procedures are so useful that we introduce them here. 4.5.2 Tracing DrRacket provides a more automated way to observe applications of procedures. We can use tracing to observe the start of a procedure evaluation (including the procedure inputs) and the completion of the evaluation (including the output). To use tracing, it is necessary to first load the tracing library by evaluating this expression: (require racket/trace) This defines the trace procedure that takes one input, a constructed procedure (trace does not work for primitive procedures). After evaluating (trace proc), the interpreter will print out the procedure name and its inputs at the beginning of every application of proc and the value of the output at the end of the application evaluation. If there are other applications before the first application finishes evaluating, these will be printed indented so it is possible to match up the beginning and end of each application evaluation. For example (the trace outputs are shown in typewriter font), > (trace factorial) > (factorial 2) (factorial 2) |(factorial 1) | (factorial 0) | 1 |1 2 2 The trace shows that (factorial 2) is evaluated first; within its evaluation, (factorial 1) and then (factorial 0) are evaluated. The outputs of each of these applications is lined up vertically below the application entry trace. 70 4.5. Developing Complex Programs Exploration 4.2: Recipes for π The value π is the defined as the ratio between the circumference of a circle and its diameter. One way to calculate the approximate value of π is the GregoryLeibniz series (which was actually discovered by the Indian mathematician Mādhava in the 14th century): π= 4 4 4 4 4 − + − + −··· 1 3 5 7 9 This summation converges to π. The more terms that are included, the closer the computed value will be to the actual value of π. a. [?] Define a procedure compute-pi that takes as input n, the number of terms to include and outputs an approximation of π computed using the first n terms of the Gregory-Leibniz series. (compute-pi 1) should evaluate to 4 and (compute-pi 2) should evaluate to 2 2/3. For higher terms, use the built-in procedure exact->inexact to see the decimal value. For example, (exact->inexact (compute-pi 10000)) evaluates (after a long wait!) to 3.1414926535900434. The Gregory-Leibniz series is fairly simple, but it takes an awful long time to converge to a good approximation for π — only one digit is correct after 10 terms, and after summing 10000 terms only the first four digits are correct. Mādhava discovered another series for computing the value of π that converges much more quickly: π= √ 12 ∗ (1 − 1 1 1 1 − . . .) + − + 3 ∗ 3 5 ∗ 32 7 ∗ 33 9 ∗ 34 Mādhava computed the first 21 terms of this series, finding an approximation of π that is correct for the first 12 digits: 3.14159265359. b. [??] Define a procedure cherry-pi that takes as input n, the number of terms to include and outputs an approximation of π computed using the first n terms of the Mādhava series. (Continue reading for hints.) To define faster-pi, first define two helper functions: faster-pi-helper, that takes one √ input, n, and computes the sum of the first n terms in the series without the 12 factor, and faster-pi-term that takes one input n and computes the value of the nth term in the series (without alternating the adding and subtracting). (faster-pi-term 1) should evaluate to 1 and (faster-pi-term 2) should evaluate to 1/9. Then, define faster-pi as: (define (faster-pi terms) (∗ (sqrt 12) (faster-pi-helper terms))) This uses the built-in sqrt procedure that takes one input and produces as output an approximation of its square root. The accuracy of the sqrt procedure7 limits the number of digits of π that can be correctly computed using this method (see Exploration 4.1 for ways to compute a more accurate approximation for the square root of 12). You should be able to get a few more correct digits than 7 To test its accuracy, try evaluating (square (sqrt 12)). Chapter 4. Problems and Procedures 71 Mādhava was able to get without a computer 600 years ago, but to get more digits would need a more accurate sqrt procedure or another method for computing π. The built-in expt procedure takes two inputs, a and b, and produces ab as its output. You could also define your own procedure to compute ab for any integer inputs a and b. c. [? ? ?] Find a procedure for computing enough digits of π to find the Feynman point where there are six consecutive 9 digits. This point is named for Richard Feynman, who quipped that he wanted to memorize π to that point so he could recite it as “. . . nine, nine, nine, nine, nine, nine, and so on”. Exploration 4.3: Recursive Definitions and Games Many games can be analyzed by thinking recursively. For this exploration, we consider how to develop a winning strategy for some two-player games. In all the games, we assume player 1 moves first, and the two players take turns until the game ends. The game ends when the player who’s turn it is cannot move; the other player wins. A strategy is a winning strategy if it provides a way to always select a move that wins the game, regardless of what the other player does. One approach for developing a winning strategy is to work backwards from the winning position. This position corresponds to the base case in a recursive definition. If the game reaches a winning position for player 1, then player 1 wins. Moving back one move, if the game reaches a position where it is player 2’s move, but all possible moves lead to a winning position for player 1, then player 1 is guaranteed to win. Continuing backwards, if the game reaches a position where it is player 1’s move, and there is a move that leads to a position where all possible moves for player 2 lead to a winning position for player 1, then player 1 is guaranteed to win. The first game we will consider is called Nim. Variants on Nim have been played widely over many centuries, but no one is quite sure where the name comes from. We’ll start with a simple variation on the game that was called Thai 21 when it was used as an Immunity Challenge on Survivor. In this version of Nim, the game starts with a pile of 21 stones. One each turn, a player removes one, two, or three stones. The player who removes the last stone wins, since the other player cannot make a valid move on the following turn. a. What should the player who moves first do to ensure she can always win the game? (Hint: start with the base case, and work backwards. Think about a game starting with 5 stones first, before trying 21.) 72 4.5. Developing Complex Programs b. Suppose instead of being able to take 1 to 3 stones with each turn, you can take 1 to n stones where n is some number greater than or equal to 1. For what values of n should the first player always win (when the game starts with 21 stones)? A standard Nim game starts with three heaps. At each turn, a player removes any number of stones from any one heap (but may not remove stones from more than one heap). We can describe the state of a 3-heap game of Nim using three numbers, representing the number of stones in each heap. For example, the Thai 21 game starts with the state (21 0 0) (one heap with 21 stones, and two empty heaps).8 c. What should the first player do to win if the starting state is (2 1 0)? d. Which player should win if the starting state is (2 2 2)? e. [?] Which player should win if the starting state is (5 6 7)? f. [??] Describe a strategy for always winning a winnable game of Nim starting from any position.9 The final game we consider is the “Corner the Queen” game invented by Rufus Isaacs.10 The game is played using a single Queen on a arbitrarily large chessboard as shown in Figure 4.5. On each turn, a player moves the Queen one or more squares in either the left, down, or diagonally down-left direction (unlike a standard chess Queen, in this game the queen may not move right, up or up-right). As with the other games, the last player to make a legal move wins. For this game, once the Queen reaches the bottom left square marked with the ?, there are no moves possible. Hence, the player who moves the Queen onto the ? wins the game. We name the squares using the numbers on the sides of the chessboard with the column number first. So, the Queen in the picture is on square (4 7). g. Identify all the starting squares for which the first played to move can win right away. (Your answer should generalize to any size square chessboard.) h. Suppose the Queen is on square (2 1) and it is your move. Explain why there is no way you can avoid losing the game. i. Given the shown starting position (with the Queen at (4 7), would you rather be the first or second player? j. [?] Describe a strategy for winning the game (when possible). Explain from which starting positions it is not possible to win (assuming the other player always makes the right move). k. [?] Define a variant of Nim that is essentially the same as the “Corner the Queen” game. (This game is known as “Wythoff’s Nim”.) Developing winning strategies for these types of games is similar to defining a recursive procedure that solves a problem. We need to identify a base case from 8 With the standard Nim rules, this would not be an interesting game since the first player can simply win by removing all 21 stones from the first heap. 9 If you get stuck, you’ll find many resources about Nim on the Internet; but, you’ll get a lot more out of this if you solve it yourself. 10 Described in Martin Gardner, Penrose Tiles to Trapdoor Ciphers. . .And the Return of Dr Matrix, The Mathematical Association of America, 1997. 73 7 í ê 3 4 0 1 2 3 4 5 ç 6 Chapter 4. Problems and Procedures « 0 1 2 5 6 7 Figure 4.5. Cornering the Queen. which it is obvious how to win, and a way to make progress fro m a large input towards that base case. 4.6 Summary By breaking problems down into simpler problems we can develop solutions to complex problems. Many problems can be solved by combining instances of the same problem on simpler inputs. When we define a procedure to solve a problem this way, it needs to have a predicate expression to determine when the base case has been reached, a consequent expression that provides the value for the base case, and an alternate expression that defines the solution to the given input as an expression using a solution to a smaller input. 74 I’d rather be an optimist and a fool than a pessimist and right. Albert Einstein 4.6. Summary Our general recursive problem solving strategy is: 1. Be optimistic! Assume you can solve it. 2. Think of the simplest version of the problem, something you can already solve. This is the base case. 3. Consider how you would solve a big version of the problem by using the result for a slightly smaller version of the problem. This is the recursive case. 4. Combine the base case and the recursive case to solve the problem. For problems involving numbers, the base case is often when the input value is zero. The problem size is usually reduced is by subtracting 1 from one of the inputs. In the next chapter, we introduce more complex data structures. For problems involving complex data, the same strategy will work but with different base cases and ways to shrink the problem size.