Recitation #6: Tail Recursion, Composition, Informal Substitution Model

Written by Alan Shieh

Tail recursion

A function that returns the value of its recursive call is said to be tail recursive. The advantage is that a tail recursive function can be compiled into a for loop. We will see exactly why later in the course, but for now just notice the following difference between the sum and sum' functions above. In the first sum function, after the recursive call returned its value, we add x to it. In the tail recursive sum', after the recursive call, we immediately return the value returned by the recursion. Once we make the recursive call, we don't need to remember anything about the current call. So in the second case, the compiler can change this recursive call into a simple goto statement. The result is that tail recursive functions tend to run faster than their standard counterparts.

Here are two versions of sum. One is tail recursive, while the other is not

  fun sum (x: int list): int  =
    case x of
	[] => 0
      | x::xs => x + sum(xs)

After the recursive call returns, x must be added to the result. So for each recursive call, we must remember an element of x. Here is the tail recursive version:

  fun sum (x: int list): int  =
    let fun helper (x: int list, accum: int): int = 
	case x of
	    [] => accum
	  | x::xs => helper(xs, x + accum)
    in
	helper(x, 0)
    end

Notice that the value of x is remembered in the accumulator. Once the recursive call returns, the caller can simply return the return value; no further computation is needed.

Notice also that foldl is tail recursive whereas foldr is not:

  fun foldl (f:'a*'b->'b) (accum:'b) (l:'a list):'b =
    case l of
      [] => accum
    | x::xs => foldl f (f(x,accum)) xs
  
  fun foldr (f:'a*'b->'b) (accum:'b) (l:'a list):'b =
    case l of
      [] => accum
    (* must remember x so that it is available once recursive call returns *)
    | x::xs => f(x, (foldr f accum xs))

Typically when given a choice between using the two functions, you should use foldl for performance. However, in many cases using foldr is easier, as in the concat function:

  fun concat (l:string list) = foldr (op ^) "" l

If you need to use foldr on a very lengthy list, you may instead want to reverse the list first and use foldl. So, we could have written concat as

  fun concat (l:string list) = foldl (op^) "" (rev l)

which would be fully tail recursive. Recall that rev can be implemented with foldl

More currying: Compose function

Let's look at currying again:

  fun compose f = fn g => fn x => f(g(x))

The sugared form:

  fun compose f g x = f(g(x))

One can also use the built-in infix operator o to compose:

  val fg = f o g

Like other curried functions, compose is useful when one wishes to define combinations of functions that will be used often:

  val printInt = print o Int.toString
  val concat : string list->string = (foldl (op^) "") o rev

Note how the way o is curried allows us to write clean code.

Informal introduction to substitution

Let's revisit substitution again with a more complex example than last time. You will start seeing a formal model of substition in the next lecture.

  fun ntimes(f: 'a -> 'a, n: int): 'a -> 'a =
    fn x => case n of
              0 => x
            | _ => f((ntimes(f, n-1)) x)

Let's see how ntimes(inc,0) is evaluated. Recall that during function invocation, we substitute in the argument values:

  fn x => case 0 of
            0 => x
          | _ => inc((ntimes(inc, 0-1)) x)

We do not yet evaluate the body of the anonymous function!

Notice that the evaluation only transforms the anonymous function that we eventually return; x doesn't yet enters the picture. Now we know how to evaluate the expression

  (ntimes inc 0) 7

We repeat the evaluation above to yield the function value

  (fn x => case 0 of
            0 => x
          | _ => inc((ntimes(inc, 0-1)) x)) 7

Substituting the argument value for 'x' in the function body, and evaluating the case

  case 0 of
    0 => 7 (* this pattern matches *)
  | _ => inc((ntimes(inc, 0-1)) 7)

    7

Let's try a more complicated example that uses the recursive structure of the function. We'll start with

  val g = ntimes(inc, 2)

We evaluate the value to bind to g. Substitute in the argument values inc and 2:

  val g = fn x => case 2 of
            0 => x
          | _ => inc((ntimes(inc, 2-1)) x)

Now let's evaluate g 3

  (fn x => case 2 of
          0 => x
        | _ => inc((ntimes(inc, 2-1)) x)) 3

Substitute argument value

  case 2 of
    0 => 3
  | _ => inc((ntimes(inc, 2-1)) 3))

Evaluate case

  inc((ntimes(inc, 2-1)) 3))

Evaluate arguments to inc *

  (ntimes(inc, 1)) 3

Evaluate arguments to ntimes so we have a function to apply

  (fn x => case 1 of
            0 => x
          | _ => inc((ntimes(inc, 1-1)) x)) 3

Applying function

  case 1 of
      0 => 3
    | _ => inc((ntimes(inc, 1-1)) 3) (* pattern matches this *)

Evaluating case

  inc((ntimes(inc, 0)) 3)

Recall the previous result of evaluatign ntimes(f, 0)

  inc((fn x => case 0 of
            0 => x
          | _ => inc((ntimes(inc, 0-1)) x) 3)

  inc 3

  4

Recall that we wanted this value so that we can apply inc (see * above)

  inc(4)

  5

Church numerals

Please e-mail me for additional notes on Church numerals, which I only covered in section #3.