Lecture 7: Recursion and the Substitution Model

Administrivia

RDZ is away next week.

Let recap

We can restate the rule for evaluating let more simply:

let val x:t = v in e end --> e{v/x}

Similarly, consider a call to an anonymous function:

(fn( id )=> e )( v )  -->  e{v/id}


Top-level loop, and variable scope

Remember that to find the value of a variable we just look textually (lexically) "out" from the piece of code where it is referenced, and find the first binding occurrence.

As someone pointed out last lecture, it’s not quite right to say that you look “up”, you really have to look “out”. Example: (again, don’t write code like this!!)

let val x =
  (let val x = 3 in x+6 end)
in
  x
end

You can think of the ML interpreter as having a giant LET statement before your code that gives lots of things their bindings. A decent model of val at top level is that it adds to this set of definitions.

val x = 27;
 
E 
 
==
 
let val x:int = 27 in 
  E
end

So we can say

val triple = (fn(z:int) => 3 * z);
 
triple(14) 
 
==
 
let val triple:int->int =  (fn(z:int) => 3 * z) in 
  triple(14)
end
 

Examples of higher-order procedures and the RSM

To start with, we could naturally assume that fun works as follows. Consider an example:

   let fun triple(z:int):int = 3 * z in triple(14) end 

 

We will assume FOR THE MOMENT that this is shorthand for saying

 
  let 
     val triple:int->int = (fn(z:int)=> 3 * z) 
  in 
     triple(14)
  end

The RSM allows us to do fun things with Higher Order Procedures, like:

fun apply_twice (f:int->int, x:int):int =
   f (f (x))
   
val x = apply_twice(triple,4)                                (* 36 *)

By RSM, triple(triple(4)) = triple(4+4+4)= (4+4+4)+(4+4+4)+(4+4+4)

fun new_mul9(z:int):int =
   apply_twice(triple,z)
 

We can also do this using functions that return functions!

type base = int
type func = base -> base
 
fun twice_func (f:func):func =
    fn (x:base) => f (f (x))
 

This function returns an anonymous procedure!

How do we use it?

val newer_mul9 = twice_func(triple);
    
fun compose (f:func, g:func):func =
    fn (x:base) => f (g (x))
 
fun twice_func2(f:func):func =
  compose (f,f)
 
val newest_mul9 = twice_func2(triple);
 

Evaluation of 'fun'

With this definition, though, recursion doesn’t work. Try this:

 
  let
     val fact = fn(z:int):int => if (z = 1) then z else z * fact (z – 1)
  in
     fact(3)  
  end

 

How can we fix this?

Our syntax will now include fun as well:

Expressions:
 
       e ::= c                       (* constants *)
           | id                      (* variables *)
           | let d in e end          (* let expressions *)
           | (fun id(id1:t1):t2 = e) (* recursive functions *)
Declarations: 
       d ::= val id = e             (* value declarations *)
            | fun id(id1:t1):t2 = e (* function declarations *)  

Eval rule for fun

Rule #D2[fun declarations]: to evaluate a function declaration fun f(x:t):t' =e, return the substitution that maps f to the function expression fun f(x:t):t'=e.

eval_decl(fun f(x:t):t' = e) = [(f,fun f(x:t):t'=e)]

Rule #E8 [fun expressions]: to evaluate a recursive function expression (fun f(x) = e), we "unroll" the function once.  In other words, we return the anonymous function (fn (x) => e') where e' is the result of substituting (fun f(x) = e) for the identifier f within the body e.  [I will drop types due to the annoying fact that fn’s syntax for return types is obscure, but it is easy to re-insert them]

eval(fun f(x) = e) = (fn (x) => e') where
     substitute([f,(fun f(x) = e)],e) = e'. 

Note that in the non-recursive case, this simplifies to the rule for fun we showed before (i.e., the call to substitute returns its second argument, so e’ = e). In other words, if f doesn’t appear in e (i.e. the function is not recursive) then the substitution does nothing and we turn fun into fn.

Examples:

Consider:

 
eval(let fun fact(z) = if (z = 1) then z else z * fact (z – 1) in fact(3) end)
 
We will let BODY = if (z = 1) then z else z * fact (z – 1)
By E7 Þ
 
eval((fun fact(z) = BODY)) (3))
 
By E8 Þ
 
eval((fn (z) => 
   if (z = 1) then z else z * (fun fact(z) = 
          BODY) (z – 1)) (3))
 
By E3 Þ
 
eval(if (3 = 1) then 3 else 3 * (fun fact(z) = 
        BODY) (3 - 1)))
 
By E6 Þ
 
eval(3*(fun fact(z) = BODY) (2))
 

As we can see, the evaluation model is not as trivial as we thought it was. Note that there end up being deferred evaluations here; we can’t multiply 3 * fact(2) until we know what fact(2) is. In fact (as it were), this allows us to give a precise definition of what it means for a function to be tail recursive: a function is tail-recursive if the size of the expression, using the RSM, does not depend on the input.

Now, try to use this new evaluation rule with other recursive funtions, like repeated.

fun repeated(f:func,n:int):func = 
   if (n=0)
   then(fn(x:base) => x)
   else compose (f, repeated(f,n-1))
 

This is a lot of fun (hah hah) to try in section…

Use of the Semantics

We have now a precise semantics for our ML subset, we can use our semantic in different areas. In particular we use it to prove properties of programs, i.e. correctness and efficiency.

There are 2 key tools for this. The first one is our precise semantics; without this, we have no definition of what an ML program does, hence no hope of proving anything about it. Our semantics is precise enough so that one can prove things with arbitrary detail; it’s good to know how to do this, but you won’t be required to do this in the course.

The second key tool is, of course, induction. Almost all proofs in CS are based on induction.

Induction summary (for proofs of program correctness).

We are going to prove a mathematical property of a program, with respect to a semantics. For example we might prove that fact(n) = n!. To say this with complete precision, what we are proving is:

For all integers n > 0,

  eval((fun fact(z) = 
             if (z = 1) then z else z * fact (z - 1)) (n)) = n!

This is, to be precise, an infinite set of statements, one for each value of n. Above, we started to prove it for n = 3. Not much fun for n = 106, etc…

We will prove this statement by induction. This means that for all n Î Z+, we will prove that fact(n) = n! We’ll do this just once, rather than À0 times.

An induction proof has 4 parts:

  1. A precise statement of what we are proving (given above, in our example). We may call this statement P[n]; it is a mathematical statement about n.
  2. What set N we are doing induction over, and what variable we are using to represent an element of that set. Often, there is more than one possibility; here, only N = Z+, induction on n.
  3. A proof for the base case, i.e. P[z] where z is the smallest element of N. Typically z = 1 (sometimes z = 0). More precisely, a proof of P[1] in our example.
  4. A proof that for any m Î N, P[m] Þ P[m+1] (weak in induction). Note that this is not the same as ("m Î N, P[m]) Þ ("m Î N, P[m+1]), which is true for any P on any infinite set. Instead we pick an arbitrary m, assume P[m] is true (Induction Hypothesis) and prove P[m+1]

Example: fact(n) = n!

We clearly state each the four steps

  1. fact(n) = n!
  2. Induction on n in Z+
  3. We need to show fact(1) = 1. See below.
  4. We need to show that for any m in Z+, fact(m) = m! => fact(m+1) = (m+1)! See below.

Now, to make life a little easier we can introduce a shortcut and write BODY (in a box!) instead of if (z = 1) then z else z * fact (z - 1). On homework or on the exams, if you do this, we expect you to clearly state what BODY (in a box!) is.

Then we do the base case.

  eval((fun fact(z) = 
             BODY) (1))
 
Þ
 
  eval((fn (z) => 
    if (z = 1) then z else z * 
        (fun fact(z) = 
      BODY) (z – 1)) (1))
 
Þ
 
  eval(if (1 = 1) then 1 else 1 * 
        (fun fact(z) = 
                   BODY) 
                (1 - 1)))
 
Þ
 
  1

 

Then we will continue proving the inductive step:

 
  eval((fun fact(z) = 
             BODY) (m+1))
 
Þ
 
  eval((fn (z) => 
    if (z = 1) then z else z * 
        (fun fact(z) = BODY) (z – 1)) 
     (m+1))
 
Þ
 
  eval(if (m+1 = 1) then m+1 else (m+1) * 
        (fun fact(z) = BODY)(m + 1 - 1)))
 
Þ

We know that m Î Z+, then m >= 1 and m+1>1, therefore the if statemante results false and will evaluate the second expression.

  eval((m+1) * (fun fact(z) = BODY) (m))
 
Þ
 
  eval(eval(m+1) * eval((fun fact(z) = BODY)(m)))

 

Now we apply our Induction Hypothesis.

Þ
 
  eval((m+1) * m!)
 
Þ
 (m+1)!