Recursion

We now will consider functions that generate computation of varying size depending upon the input.  The technique used to achieve this is called recursion where our algorithm breaks up the task into smaller instances of the original task and some how combines the results.

We will illustrate this with the task of making a paper chain.  Stand in a line and I will be the original customer asking for a paper chain of some size, n.  Grab your paper slips and a stapler!

Make a chain of length n:

  1. If n=1
    1. Bend a strip around to bring the ends together and join them
    2. Deliver your chain of length 1 to your customer
  2. Otherwise,
    1. Pick up a strip
    2. Ask the person next to you to make you a chain of length n-1
    3. When you get your chain, slip your strip through one end, bend it around, and join the ends.
    4. Deliver your chain of length n to your customer.

Notice that the problem of making a chain was broken up into two cases:  a base case of length 1 and the recursive case where we used the same algorithm to solve a smaller instance of the problem (making a chain of length n-1) and then did some extra work on the result to create our chain.  This type of strategy is called linear recursion.  Let's look at some examples in Haskell.

Suppose we want to compute the number of different orderings of a deck of cards.  We would have 52 possibilities for the first card, 51 possibilities, for the second card, 50 possibilities for the third card, etc.  Therefore the number of orderings would be 52 * 51 * 50 * ... * 1,  or 52 factorial, written 52!  Here is a Haskell function to compute n factorial.

fact :: Int -> Int
fact n = if n==1
    then 1
    else n * fact (n-1)

Notice the definition is broken up into a base case, when n is 1, and the recursive case which uses the smaller instance of the problem, fact (n-1).  Let's use the substitution model to examine the computation of fact 4:

fact 4  =>                                            -- substitute into the body of fact
4 * fact 3  =>                                     --
substitute for fact 3
4 * (3 * fact 2)  =>                          
-- substitute for fact 2
4 * (3 * (2 * fact 1))  =>                 
-- substitute for fact 1
4 * (3 * (2 * 1))  =>                        
4 * 3 * 2    =>
4 * 6  =>
24

Observe that the computation is composed of a winding phase where the expressions are getting larger and larger with each recursive substitution, and an unwinding phase where the results are being combined.

Let's look at an example that uses Words.  The following function reverses the letters in a word:

revWord :: Language -> Language
revWord wd = if (empty wd)
    then wd
    else (lastItem wd) +++ revWord (butLast wd)

Use the substitution model to look at the computation of revWord (word "cat").

Try writing linear recursive functions for the following:

-- compute the base value raised to the given power
power :: Int -> Int -> Int
power base exp = ...

-- compute the number of letters in a word
length :: Language -> Int
length w = ...

Another type of recursion feels more like the iteration (loops) with which you are already familiar.  Look at another version of the factorial function:

factTail :: Int -> Int
factTail n = fTail n 1 where
    fTail n result =
        if n == 1
            then result
            else fTail (n-1) (result * n)

This type of recursion is called tail recursion and we defined a helper function with two arguments.  The second argument is used to build up the resulting computation.  Notice that each time we recursively used fTail, we simply modified each of the two arguments.  Let's look at the computation of factTail 4 with the substitution model:

factTail 4  =>                                    -- substitute into the body of factTail
fTail 4 1 =>                                       --
substitute for fTail
fTail 3 4 =>                                       --
substitute for fTail
fTail 2 12 =>                                    --
substitute for fTail
fTail 1 24 =>                                    --
substitute for fTail
24

In tail recursion there is no winding and unwinding.

Let's do a tail recursive version of revWord.

revWordTail :: Language -> Language
revWordTail wd = revTail wd (word "") where
    revTail wd result =
        if (empty wd)
            then result
            else revTail (butFirst wd) ((firstItem wd) +++ result)

Use the substitution model to look at the computation of revWordTail (word "cat").

Try writing tail recursive functions powerTail and lengthTail.