Designing Recursive Function Definitions


Look for similar subproblems

As an illustration of how to derive function definitions, let's start with a somewhat odd but simple problem. Given an integer, you would like to know how many 7's there are when you write it in the usual form (base 10). For example, 271 has one 7 in it, 777 has three sevens, and 25 has none.

The first thing to do is to make sure that you understand the problem. Try some examples, and make sure that you can do those. We have tried a few examples already. 7071 has two 7's.

Next, look for smaller versions of the same kind of problem inside larger ones. Often, you ask yourself first how to find the smaller problems, and then how to make effective use of their solutions. Suppose that you find the smaller problem by subtracting one. For example, starting with 277, subtract one to get 276. Does that help? Well, it does not look very promising. The number of 7's in n - 1 does not seem to tell us much about the number of 7's in n.

But recall that n `mod` 10 is the rightmost digit of n, and n `div` 10 is what you get by removing the rightmost digit. For example,

  277 `mod` 10 = 7
  277 `div` 10 = 27
Suppose the rightmost digit is a 7, as it is in 277. Then, to count the 7's in 277, you just count the 7's in the smaller number 27, then add 1 to count the 7 that you took off. Similarly, to count the 7's in 7777, just count the three 7's in the smaller number 777, then add 1 to account for the extra 7 that you removed. Let's write down this observation.
If the rightmost digit of n is a 7, then the total number of 7's in n is one larger than the total number of 7's in (n `div` 10).

Now suppose that the rightmost digit is not a 7. Then just get rid of it, and count the number of 7's in what is left. For example, the number of 7's in 375 is the same as the number of 7's in 37.

If the rightmost digit of n is not a 7, then the total number of 7's in n is the same as the total number of 7's in (n `div` 10).

We are almost ready. But there is generally at least one very small value that needs special treatment. For this problem, that small case is 0. The rightmost digit of 0 is not a 7, and the rule above says to compute 0 `mod` 10 = 0 and ask how many 7's are in that. But then we are just passing the buck. Recursion only works when you look at smaller values than you started with, and 0 is not smaller than 0.

That is not really a problem. It is just a matter of introducing a third rule to handle this case.

The number of 7's in 0 is 0.

Lets call the function numSevens(n). Here are the three cases, translated to Cinnameg.

  case numSevens(n) = 0                          when n == 0
  case numSevens(n) = 1 + numSevens(n `div` 10)  when n `mod` 10 == 7
  case numSevens(n) = numSevens(n `div` 10)

Let's test this by doing a full computation of numSevens(1707).

  numSevens(1707)
    = 1 + numSevens(1707 `div` 10)     (by the second case,
                                        since 1707 == 0 is false and 
                                        1707 `mod` 10 == 7 is true)
    = 1 + numSevens(170)
    = 1 + numSevens(170 `div` 10)      (by the third case,
                                        since 170 == 0 is false and 
                                        170 `mod` 10 == 7 is false)
    = 1 + numSevens(17)
    = 1 + (1 + numSevens(17 `div` 10)) (by the second case,
                                        since 17 == 0 is false and 
                                        17 `mod` 10 == 7 is true)
    = 1 + (1 + numSevens(1))
    = 1 + (1 + numSevens(1 `div` 10))  (by the third case,
                                        since 1 == 0 is false and 
                                        1 `mod` 10 == 7 is false)
    = 1 + (1 + numSevens(0))           
    = 1 + (1 + 0)                      (by the first case)
    = 1 + 1
    = 2

Of course, a better approach is to assume that the function works correctly for smaller values than the one we started with. Here is an abbreviated (and perfectly reasonable) hand simulation of numSevens(1707).

  numSevens(1707)
    = 1 + numSevens(1707 `div` 10)     (by the second case,
                                        since 1707 == 0 is false and 
                                        1707 `mod` 10 == 7 is true)
    = 1 + numSevens(170)
    = 1 + 1                            (since 170 is smaller than 1707, and
                                        170 has just one 7)
    = 2


Another example

This time, suppose you want to find out whether a number has any 7's in it. This time, the answer is either true or false. For example, 39 has no sevens, so we will say that hasASeven(39) is false. But 775 does have a 7, so hasASeven(775) is true.

Using the same ideas as the for the preceding problem, there are three cases to consider for hasASeven(n). Either n is 0, or the rightmost digit of n is a 7, or the rightmost digit of n is not a 7. With just a little thought, the answers should be clear for each case

  case hasASeven(n) = false                  when n == 0
  case hasASeven(n) = true                   when n `mod` 10 = 7
  case hasASeven(n) = hasASeven(n `div` 10)
Let's see how hasASeven(3745) works.
  hasASeven(3745)
    = hasASeven(3745 `div` 10)
    = hasASeven(374)
    = hasASeven(374 `div` 10)
    = hasASeven(37)
    = true


Case study: greatest common factor

The greatest common factor of two integers x and y is the largest integer that is a factor of both x and y. For example, the greatest common factor of 30 and 14 is 2 and the greatest common factor of 20 and 30 is 10. Let's use gcf(x, y) to mean the greatest common factor of x and y.

The Greek mathematician Euclid noticed some facts about greatest common factors. First, 0 is divisible by every integer, since n*0 = 0 for every n. So gcf(0, x) = x.

Second, he noticed that gcf(x,y) = gcf(y `mod` x, x). For example, gcf(20,30) = gcf(10,20). (The reason is that, when you compute the remainder when you divide y by x, you have only removed something that is divisible by x. So what you have removed is also divisible by gcf(x,y). With a little thought, you can reconstruct Euclid's argument.)

Putting Euclid's observations together suggests the following definition of gcf(x,y).

  case gcf(x,y) = y                   when x == 0
  case gcf(x,y) = gcf(y `mod` x, x) 
But does that work? First, let's try to get gcf(30, 14) doing a step-by-step evaluation.
  gcf(30,14) =
    = gcf(14 `mod` 30, 30)
    = gcf(14, 30)
    = gcf(30 `mod` 14, 14)
    = gcf(2, 14)
    = gcf(14 `mod` 2, 2)
    = gcf(0, 2)
    = 2
At least it works for that example. If Euclid was right, then this function cannot produce any incorrect answers. But it is important that the numbers get smaller as you go. Remember that you can choose either x or y to look at, to be sure that it gets smaller. Notice that the first argument, x, seems to get smaller each time. For example, you go from computing gcf(30, 14) to gcf(14, 30) to gcf(2, 14) to gcf(0, 2). At each step, the first argument gets smaller. Why? Moving from gcf(x,y) to gcf(y `mod` x, x), as is done in the second case, replaces the first argument x by y `mod` x But the remainder when you divide anything by x is always smaller than x. So the first argument got smaller.


Avoid "random" programming

If you are tired and frustrated, you will probably find that you stop thinking carefully and start trying random things to see whether they will work. You end up saying to yourself "I don't really need to understand why it works. I just need to stumble on something that happens to work."

If you ever find yourself in that frame of mind, stop what you are doing. Put the program down and take a break. Come back to it when you are willing to think about the problem and figure out the answer.

Students have spent countless hours on fruitless random programming. It does not work, and is a waste of time. Do not wait until you have frittered away days of your time on it before believing that.


Problems

  1. [solve] Would definition

      case gcf(x,y) = y                   when x == 0
      case gcf(x,y) = gcf(x, y `mod` x) 
    
    work for finding the greatest common factor of two numbers? Explain. (Hint: Try computing gcf(30,14) with this definition. What happens?)

  2. [solve] Write a definition of function sumOdds(n) which produces the sum of the odd integers that are less than or equal to n. Assume that n is an odd positive integer. For example, sumOdds(3) = 1 + 3 = 4, and sumOdds(7) = 1 + 3 + 5 + 7 = 16. Use recursion.

    (Hint. For n = 1, just say what the answer is. For n > 1, look for a smaller problem of the same kind that will help you, and use the same function to solve that smaller problem.)

  3. [solve] Write another definition of function sumOdds(n) which produces the sum of the odd integers that are less than or equal to n. But this time do not assume that n is odd, only assume that it is positive. For example, sumOdds(4) = 1 + 3 = 4, and sumOdds(8) = 1 + 3 + 5 + 7 = 16. This new sumOdds must still work for odd integers too. For example, sumOdds(9) = 1 + 3 + 5 + 7 + 9 = 25.

    (Hint. If n is even, just compute sumOdds(n-1).)

  4. [solve] Suppose that a function isPrime(n) is available to you, which yields true if n is a prime number. (An integer n is prime if n > 1 and n is only divisible by itself and 1.) The first seven prime numbers are 2, 3, 5, 7, 11, 13 and 17.

    Write a definition of function numPrimes(n) that produces a count of the number of prime integers that are less than or equal to n. Use recursion. (For this problem, function isPrime(n) is provided for you.)

    Hint.

    1. What is numPrimes(n) when n < 2?

    2. When n is prime, numPrimes(n) is one larger than numPrimes(n-1). For example, numPrimes(4) = 2 and numPrimes(5) = 3, since numPrimes(5) counts one more prime number, 5.

    3. When n is not prime, numPrimes(n) is the same as numPrimes(n-1). For example, numPrimes(7) = 4 and numPrimes(8) = 4.


Summary

To develop a function definition that involves recursion, think about cases. Try to exploit solutions to smaller versions of the same kind of problem. There will always be at least one special case where you do not use the function that you are defining. Those are for very small values, and are usually easy to deal with.


Review

When checking a recursive definition, make sure that you first know what you think it does. Then, when doing a check, assume that you are right all arguments that are smaller than the argument that you started with. If there are two or more arguments, choose one of them. You can assume that you are right when that one has decreased.

Recursion always relies on using more than one case.