Lecture 8:
The Substitution Model - Recursive Functions

Feedback on HW2

We have graded homework 2, and grades are already available on CMS. We will return them tomorrow in section.

The course staff has noticed a number of typical errors, and I want to spend a few minutes addressing them before we continue with the substitution model.

Finally, let me remind you to read the cs312 newsgroup messages. These cover many topics of general interest; all of you can save time and implementation effort if you are aware of them.

Substitution Model - Recursive Functions

As you recall, the substitution model handles a subset of the SML language, and it consists of a set of rules that specify the semantics of SML programs. The rules can be applied mechanically, and each step is unambiguously determined by the structure of the expression that is evaluated. This feature of the subtitution model can serve as the basis for implementing an SML interpreter. Of course, the interpreter does not necessarily has to follow the actions specified by the substitution model, it just has to behave so that an outside observer experiences the same program behavior as if the interpreter implemented the substitution model exactly.

Last time we have provided a set of rules for evaluating expressions and val declarations in a subset of SML. An important element of our model is still missing, namely, the rule that specifies the behavior of (possibly recursive) named functions. We turn our attention to this topic now.

Rule #D2[fun declarations]

eval_decl(fun f x = e) = [(f, fun f x = e)]

Evaluating a function declaration is equivalent to creating a substitution that associates the function name with its definition.

Rule #E8[fun expressions]

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

The result of evaluating a fun expression is an anonymous function with one argument, denoted by the same identifier that represented the unique argument of the original function. The body of the anonymous function is obtained by substituting the free occurences of the function's name in the body of the very same function with the expression that defines the function.

To clarify this idea, here are two examples:

"But wait," you might ask, "what is a fun expression? It is not part of the language as you specified it to us!" You are right - an explanation is warranted.

First, SML does not have true fun expressions. For example, you can not write expressions like the one below:

-(fun f(x) = x + 1) 4;
stdIn:13.1-13.7 Error: syntax error: deleting  LPAREN FUN ID

We introduce fun expressions in our language to simplify the rules for the subtitution model. When we declare a value in a let, for example, we can remove the declaration and work only on the expression between in and end because we substitute all free occurences of the declared identifier with the value that it represents.

We try to do something analogous with named functions, however, the problem is complicated by the fact that named functions can be recursive, i.e. the function's name can appear as a free identifier in the body of the function itself. Because of this we can't get rid of the name of the function (as we did when we substituted the identifier in the val declaration), rather, we need to keep the whole function definition around. Instead of inventing a new mechanism for remembering the definition of named functions, we choose to embed this definition wherever the function name appears. It is a workaround, but it solves our problem.

For completeness, we now return to the definition of expressions we gave in the previous lecture and we add fun expressions to the list:

e ::= c                        (* constants *)
    | id                       (* variables *)
    | (fn id => e)             (* anonymous functions *) 
    | e1(e2)                   (* function applications *)
    | u e                      (* unary operations, ~, not, etc. *)
    | e1 b e2                  (* binary operations, +,*,etc. *)
    | (if e then e1 else e2)   (* if expressions *)
    | let d in e end           (* let expressions *)
    | (fun f x = e)            (* fun expressions *)

Let us note that previously we had constants and anonymous functions appear both as expressions and values. With the addition of fun expressions, we now have named functions that appear as both declarations and expressions. The meaning of a fun construct will depend on the context in which it is used. As a rule of thumb, if fun appears in a let between let and in, then it is a declaration, if it appears in an expression derived from that between in and end, then it is an expression. Let us now use the substitution model to evaluate the following expression:

let
  fun fact(n) =
    if n = 0
    then 1
    else n * fact(n - 1)  
in
  fact (3)
end

As before, we will rewrite the expression to fit into one line. In addition we will denote the body of the function with BODY, like so:

BODY = if n = 1 then 1 else n * fact(n - 1)

This is a convention that you are encouraged to follow; it saves both a lot of time and space.

eval(let fun fact(n) = if n = 1 then 1 else n * fact(n - 1) in fact(3) end)
= eval(let fun fact(n) = BODY in fact(3) end)

E7.0. eval_decl(fun fact(n) = BODY)
  D2 = [f, fun fact(n) = BODY]

E7.1. substitute([f, fun fact(n) = BODY], fact(3))
     = (fun fact(n) = BODY) 3

E7.2. eval((fun fact(n) = BODY) 3)

  E8 = eval((fn n => if n = 1 then 1 else n * (fun fact(n) = BODY) (n-1)) (3))

    E3.0. eval((fn n => ...)) = (fn n => ...)

    E3.1. eval(3) = 3

    E3.2. substitute([(n, 3), if n = 1 ...]) = 
            = if 3 = 1 then 1 else 3 * (fun fact(n) = BODY) (3 - 1))

    E3.3. eval(if 3 = 1 then 1 else 3 * (fun fact(n) = BODY) (3 - 1))

      E6.0 eval(3 = 1) = false 
           [Note the shortcut above, we should have used the rule for binary operators.]
      E6.1 -- no evaluation here
      E6.2 eval(3 * (fun fact(n) = BODY) (3 - 1))

We can further simplify this derivation by showing only the global effect of each rule application.

eval(let fun fact(n) = if n = 1 then 1 else n * fact(n - 1) in fact(3) end)

     = eval(let fun fact(n) = BODY in fact(3) end)

[E7] = eval((fun fact(n) = BODY) 3)

[E8] = eval((fn n => if n = 1 then 1 else n * (fun fact(n) = BODY) (n-1)) (3))

[E3] = eval(if 3 = 1 then 1 else 3 * (fun fact(n) = BODY) (3 - 1))

[E6] = [Because eval(3 = 1) = false, we take the 'else' branch]
       eval(3 * (fun fact(n) = BODY) (3 - 1))

Tail Recursive Functions

We are not done with the application of the substitution model to the computation of fact(3) - all we did until now was, in effect, to illustrate the execution of the factorial function until the first recursive call. If we simulated the effect of the recursive call, we would end up with the following expression:

eval(3 * (2 * (fun fact(n) = BODY) (2 - 1)))

After yet another step our expression becomes:

eval(3 * (2 * (1 * (fun fact(n) = BODY) (1 - 1))))

It is easy to see that that because of the recursive function calls certain computations are "suspended" (here, the multiplications), and can not be performed until the last recursive call returns. It is easy to see that the number of suspended computations, and the complexity of the expression to be evaluated, both depend on n, the function's argument. Such a dependency is indicative of, and can be used to formally define, non-tail-recursive functions. Conversely, if the complexity of the expression to be evaluated does not depend on the input parameter, then the function is tail-recursive.

Global Scope

Our rules do not explicitly address the handling of declarations and expressions in the global scope. For now, you can think of the global scope as of a large sequence of nested let statements, containing one declaration each. These let statements should be nested to that all predefined functions and values are declared first, followed by the user's declarations, in the order in which he entered them.

Our uses of the substitution model will generally not rely on the existence of the global scope, except through the assumption that certain predefined functions are known. Whenever we will need to declare values, we will use a sequence of let statements for this purpose. Sometimes, for brevity, we will write let statements with more than one declaration, but we will still think of these as representing a sequence of nested lets for the purposes of the substitution model.

Lazy Evaluation and the Substitution Model

We have talked about the lazy and eager evaluation strategies. If one designs a language, choosing between lazy and eager evaluation is a fundamental decision. But how is eagerness expressed in our substitution model? It turns out that it is hidden in the function application rule E3:

Rule #E3 [function calls]:

eval(e1(e2)) = v'  where
  (0) eval(e1) = (fn id => e)
  (1) eval(e2) = v
  (2) substitute([(id,v)],e) = e'
  (3) eval(e') = v'

This rule specifies that we should evaluate function arguments before we call the function itself. Assume that we change the rule as follows:

eval(e1(e2)) = v'  where
  (0) eval(e1) = (fn id => e)
  (1) substitute([(id,e2)],e) = e'
  (2) eval(e') = v'

We first note that the evaluation step of e2 is gone; function arguments are not evaluated before the function call. This is the core of the lazy evaluation strategy. To implement it, however, we need to change the definition of step (1) ("old" step 2). Note that in the substitution we replace the identifier that represents the function argument with the expression that corresponds to it at the time of the function call. Again, no evaluation occurs at this stage.