Lecture 5: Substitution Model

Administrivia

Problem set #1 is due Friday 7PM.

Problem set #2 was handed out on Wednesday evening. Don’t do it in pairs (you can work in pairs for PS3-5).

RDZ is here next week, away the week after.

Please be sure that you post questions on the newsgroup rather than mailing them to cs312-l. The reason is that it is almost impossible for you to be the only person with your question, and this way we can share the answer with everyone.


Currying

Here is another example of why we need a semantics. For example:

val ford =
fn(x:int)=>fn(y:bool)=>fn(z:int list)=>(if y then null(z) else x = 42)
 

What is the type and value of:

 

ford  (* int->bool->int list->bool *)

ford(42) (* bool->int list->bool *)

ford(42)(true) (* int list->bool *)

ford(42)(true)[4,2] (* false *)

 

Not to mention, understanding functions that take functions as arguments and return functions as values…


Back to the Substitution Model:

Recap

syntactic class

syntactic variable(s) and grammar rule(s)

examples

constants

c

...~2, ~1, 0, 1, 2 (integers)
 
1.0, ~0.001, 3.141 (reals)
true, false (booleans)
"hello", "", "!" (strings)
#"A", #" " (characters)

unary operator

u

~, not, size, ...

binary operators

b

+, *, -, >, <, >=, <=, ^, ...

expressions (terms)

e ::=  u e  |  e1 b e2  | if e then e else e  

~0.001, not true, 2 + 2

Rule #E1 [constants]: constants evaluate to themselves

eval(c) = c

Rule #E2 [unary ops]: to evaluate u e where u is a unary operation such as not or ~, evaluate e to a value v', then perform the appropriate unary operation on the value v' to get the result v.

eval(u e) = v where
  (0) eval(e) = v'
(1)   v = apply_unop(u,v')

Rule #E3 [binary ops]: to evaluate e1 b e2 where b is a binary operation such as +, *, -, etc. Evaluate e1 to a value v1, then evaluate e2 to a value v2, then perform the appropriate operation on the values v1 and v2 to get the result v.

eval(e1 b e2) = v where
  (0) eval(e1) = v1
  (1) eval(e2) = v2
  (2) v = apply_binop(b,v1,v2)

Rule #E4 [if]: to evaluate (if e then e1 else e2), evaluate e to a value v. Then depending on the (boolean) value of v, the value is either the result of evaluating e1 or e2.

eval(if e then e1 else e2) = v' where
(0) eval(e) = v
(1) if v = true then v' = eval(e1)
(2) if v = false then v' = eval(e2)

Syntax and Semantics of Procedures

Now we need to add various things to our BNF table, to make fn part of the syntax, and to eval, to give fn the correct semantics. We also need to add identifiers, which are variable names. Both identifiers and anonymous functions are expressions, as is a particular expression called a combination. Finally, we need to add types.

syntactic class

syntactic variable(s) and grammar rule(s)

examples

identifiers

x, y

a, x, y, x_y, ford1000, ...

constants

c

...~2, ~1, 0, 1, 2 (integers)
 
1.0, ~0.001, 3.141 (reals)
true, false (booleans)
"hello", "", "!" (strings)
#"A", #" " (characters)

unary operator

u

~, not, size, ...

binary operators

b

+, *, -, >, <, >=, <=, ^, ...

expressions (terms)

e ::= x  |  u e  |  e1 b e2  | if e then e else e  | fn (x1:t1, ..., xn:tn): t =  | e (e1, ..., en)

ford, ~0.001, not b, 2 + 2

types

t ::= int  |  real  |  bool  |  string  |  char  |  t1*...*tn->t

int, string, int->int, bool*int->bool

Adding support to eval for this is subtler than it first appears. To begin with, we need to expand the definition of a value (i.e., the final result of evaluating an expression). For reasons that will eventually become clear (perhaps!), it is desirable to allow anonymous functions to be values. This results in the new rule:

Rule #E5 [functions]:  anonymous functions evaluate to themselves

eval(fn (id:t) => e) = (fn (id:t) => e)

Finally, we need to figure out what the value is of a combination. Here, the key concept is that we substitute the value of the identifier for the identifier in the body, and then evaluate that. But it’s a little trickier than it at first appears…

Rule #E6 [combinations]:  to evaluate e1(e2),  evaluate e1 to a function (fn (id:t) => e), then evaluate e2 to a value v, then substitute v for the formal parameter id within the body of the function e to yield an expression e'.  Finally, evaluate e' to a value v'.  The result is v'.

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

OK, what does it mean to substitute? The simple version is we simply replace the identifier with the value in the expression.

Does this work? On simple cases, yes. Let’s try it:

(fn(z) => z*z + 17)(2+3)

[Note: I will often drop types in lecture. Don’t do this when you are writing code!]

Looks good so far. But actually, it doesn’t work and we need to do something more subtle. Can anyone see why it doesn’t work to simply replace z in the body by 5? Well, let’s think of some other things that the could be the body of the expression…

Consider another expression that has the value 17. By referential transparency we can use this instead of 17 and get the same answer. So far so good. But now suppose that the expression we use, which has the value 17, is actually

(fn(z) => z+7)(10)

So that makes our expression

(fn(z) => z*z + ((fn(z) => z+7)(10)))(2+3)

We substitute 5 for z in the body and end up with something seriously wrong, namely 5*5 + 12 = 39. Not the answer to life at all…

Clearly we need to substitute carefully.

The simple rule is that you don’t substitute for the variable z inside a combination whose parameter is the variable z. But we can look at this in more detail.


Let

We can make this issue clearer by introducing a new feature in ML that allows us to create temporary names for variables. This new feature does not add any power beyond what fn provides, but it is very convenient.

Suppose we want to evaluate the expression E with the variable z bound to 5. We can do this straightforwardly by writing the combination

(fn(z:int) => E)(5)

Let’s try it out on an example: eval(3 * (if (1 > 2) then 5 else (7+7))

Unfortunately, this kind of code is pretty hard to read. Consider: evaluate E’ with z bound to 5 and y bound to z*z. In the above we replace E by ((fn(y)=>E’)(z*z)) thus producing the totally unreadable

(fn(z:int)=>
((fn(y:int)=>E’)(z*z))
(5)

Not fun at all. Believe it or not, some pretty famous large programs have been written using this style, including the PhD thesis of MIT’s past provost (Joel Moses).

How do we do better? Well, informal definitions of special forms are best done by example. So here’s an example:

let val z:int = 5 
in 
   E
end
 
let val z:int = 5 
in 
   let val y:int = z*z 
   in
      E’        
   end
end

Much easier to read! Note that this val declaration is needed for a language feature we haven’t yet added (but will shortly), namely fun.

In fact there is an even easier to read version of this, namely:

let val z:int = 5 
    val y:int = z*z 
in
  E’        
end

Scope, Identifiers, and Substitution

Having briefly introduced let, we can now turn our attention to the issue of what it means to substitute a value for a variable.

Scope

We can define various functions but we need to avoid collisions. Often we only "need" a certain name within a certain piece of code (literally within). Where an identifier is defined is called its scope. This issue can be very confusing when you type things into ML, as opposed to loading a file into a fresh ML.

Here is a more complex function declaration which finds (an approximation to) the square root of a real number. 

Underlying math fact: for any positive x, g, it is the case that  g, x/g lie on opposite sides of sqrt(x).

(* Computes the square root of x using Heron of Alexandria's
 * algorithm (circa 100 AD). We "guess" that the square root
 * is 1.0 and then continue improving the guess until we're
 * with delta of the real answer.  The improvement is achieved
 * by averaging the current guess with x/guess.
 *)
fun square_root(x: real): real =
  let
    (* used to tell when the approximation is good enough *)
    val delta = 0.0001
    (* returns true iff the guess is good enough *)
    fun good_enough(guess: real): bool =
      abs(guess*guess - x) < delta
    (* improve the guess by averaging it with x/guess *)
    fun improve(guess: real): real =
      (guess + x/guess) / 2.0
    (* try a particular guess -- looping and improving the
     * guess if it's not good enough. *)
    fun try_guess(guess: real): real =
      if good_enough(guess) then guess
      else try_guess(improve(guess))
  in
    (* start with a guess of 1.0 *)
    try_guess(1.0)
  end

This is example shows a number of things.   First, you can declare local values (such as delta) and local functions (such as abs, good_enough, improve, and try_guess.)  Notice that "inner" functions, such as improve, can refer to outer variables (such as x).  Also notice that later definitions can refer to earlier definitions.  For instance, try_guess refers to both good_enough and improve.  Finally, notice that try_guess is a recursive function -- it's really a loop.  It's similar to writing something like:

while (!good_enough(guess)) {
   guess = try_guess(improve(guess));
}

in an imperative language such as Java or C.

If you type the square_root declaration above into the SML top-level, it responds with:

val square_root : fn real -> real

indicating that you've declared a variable (square_root), that its value is a function (fn), and that its type is a function from reals to reals. All of the internal structure of the function definition is hidden; all we know from the outside is that its value is a simple function. In particular, the function "try" is not defined!

After typing in the function, you might try it out on a real number such as 9.0:

- square_root(9.0);
  val it = 3.00000000014 : real

SML has evaluated the expression "square_root(9.0)" and printed the value of the expression (3.00000000014) and the type of the value (real).   

At the moment we have only a sloppy, imprecise notion of exactly what happens when you type this expression into ML.  In a few weeks we'll have a precise understanding (hopefully!)

If you try to apply square_root to an expression that does not have type real (say an integer or a boolean), then you'll get a type error:

- square_root(9);
stdIn:27.1-27.14 Error: operator and operand do not agree [literal]
operator domain: real
operand:         int
in expression:
  square_root 9

Binding and identifiers

Consider an ML expression like the one below:

let val ford = 3 
    val ford:int->bool = 
            fn(ford:int) => ford = ford 
in 
 ford(3)  (* how about ford(42)? *)
end

There are three different ways that one can use an identifier:

  1. A binding occurrence, which binds the identifier to a particular value or type. For example, in the expression let val x:int = 1 in x end, the first occurrence is a binding occurrence that binds x to 1. Each binding occurrence introduces a new variable, and this new variable has a scope: a part of the program in which uses of that identifier refer to the variable. In this case the scope of the variable x is the body of the let expression.
  2. A bound occurrence is a use of a variable in the scope of a variable binding. For each bound occurrence of a variable, there is a single corresponding binding of that variable. For example, in the expression (fn(x:int)=>x),  the second occurrence of x is a bound occurrence; its corresponding binding occurrence is the first occurrence. At run time this variable will be bound to whatever value is passed to the function when it is invoked.
  3. An unbound or free occurrence is a use of an identifier with no corresponding binding occurrence in whose scope. For example, in the expression let val y:ford = x+1 in y end, the use of x is an unbound occurrence because there is no containing binding of x. The identifier ford is also an unbound occurrence of a type identifier. A legal SML program cannot contain an unbound occurrence of an identifier. However, for the purpose of understanding how SML works, sometimes it is useful to write down syntactically legal fragments of SML programs and talk about the unbound variables that occur in them.

Given an occurrence of an identifier that is not a binding occurrence, there is a simple way to figure out whether it is bound or unbound, and if the former, to which binding occurrence the identifier is bound. An identifier is bound if it is in the scope of a binding occurrence. For ML programs, the scope of a variable can be seen by simply looking at the program text. If the variable lies within the scope of more than one binding occurrence, then one of those bindings shadows the rest. It will be the binding occurrence whose scope most tightly encloses the use of the identifier.

In SML it is possible to figure out just by looking at the program code which occurrence binds each use of a variable. A language with this property is said to have lexical scoping : the scope of each variable is apparent from the lexical form of the program, without knowing anything about how the program runs. The alternative to lexical scoping is dynamic scoping, in which a given variable occurrence may have different binding occurrences depending on how the program runs. In most modern languages, such as Java or C, variable have lexical scope. However, Perl and Python are examples of languages with dynamic variable scoping. Dynamic scope is harder to implement efficiently, and can lead to unpleasant surprises for programmers because variables don't always mean what they expect.

Substitution

Earlier we saw some rewriting rules that explained how to evaluate terms of the SML language. For example, we said that a simple expression evaluates according to the following rewrite rule:

let val x:t = v in e end  -->  e (with occurrences of x replaced by v)

Remember that we use e to stand for an arbitrary expression (term), x to stand for an arbitrary identifier, and v to stand for a value -- that is, a fully evaluated term.

We now know this cannot be the full story, because e2 may contain occurrences of x whose binding occurrence is not this binding x:t = v1.  It doesn't make sense to substitute v for these occurrences. For example, consider evaluation of the expression:

let val x:int = 1
    fun f(x:int) = x
    val y:int = x+1
in
  fn(a:string) => x*2
end

The next step of evaluation replaces the green occurrences of x with 1, because these occurrences have the first declaration as their binding occurrence. Notice that the two occurrences of x inside the function f, which are respectively a binding and a bound occurrence, are not replaced. Thus, the result of this rewriting step is

let fun f(x:int) = x
    val y:int = 1+1
in
  fn(a:string) => 1*2
end

This is actually a very important example, and illustrates referential transparency. The person writing this function expects it to compute the identity! Nothing involving the variable x that happens before or after (in the code or during execution) should affect this.

Let's write the substitution e{v/x} to mean the expression e with all unbound occurrences of x replaced by the value v. To remember which way the slash goes, think of multiplication: we want x{v/x} = v, which wouldn’t work if we wrote x{x/v}.

Then we can restate the rule for evaluating let more simply:

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

This works because any occurrence of x in e must be bound by exactly this declaration val x:t = v. Here are some examples of substitution:

x{2/x}  =  2
x{2/y}  =  x
(fn(y:int)=>x) {"hi"/x}  =  (fn(y:int)=>"hi")

f(x) { fn(y:int)=>y / f } =  (fn(y:int)=>y)(x)

One of the features that makes ML fairly unique is the ability to write complex patterns containing binding occurrences of variables. Pattern matching in ML causes these variables to be bound in such a way that the pattern matches the supplied value. This is can be a very concise and convenient way of binding variables. We can generalize the notation used above by writing e{v/p} to mean the expression e with all unbound occurrences of variables appearing in the pattern p replaced by the values obtained when p is matched against v. Using this notation, we can express the let rule simply:

let val p = v in e end --> e{v/p}

Example:

let val (x,y) = (40,2) in x+y end

What if a let expression introduces multiple declarations? Such an expression is identical in effect to a series of nested let expressions. Thus, we can use the following rewrite that pulls out the first declaration so the rules above apply. 

let d1...dn  in e end  -->
  let d1 in let d2...dn in e end end

We can use the same substitution operator to give a more precise rule for what happens when a function is called. Consider a function declared as fun f(p) = e, where f is the identifier naming the function. Then the expression for a function call whose argument has been evaluated, f(v), is rewritten as follows:

f(v)  -->  e{v/p}

Similarly, consider a call to an anonymous function:

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


Let syntax and semantics

OK, we now need to add a syntax and semantics for let. Conceptually it’s pretty easy, but there are a few details. We’ll start by giving a much more complete syntax for ML, including a bunch of things whose precise semantics we won’t cover for a while, if ever (such as datatypes).

syntactic class

syntactic variables and grammar rule(s)

examples

identifiers

xy

a, x, y, x_y, foo1000, ...

datatypes, datatype constructors

X, Y

Nil, Conslist

constants

c

...~2, ~1, 0, 1, 2 (integers)
 
1.0, ~0.001, 3.141 (reals)
true, false (booleans)
"hello", "", "!" (strings)
#"A", #" " (characters)

unary operator

u

~, not, size, ...

binary operators

b

+, *, -, >, <, >=, <=, ^, ...

expressions (terms)

e ::=  x  |  u e  |  e1 b e2  | if e1 then e2 else e3  |  let d1...dn in e end  |  e (e1, ..., en)  | (e1,...,en)  | #n e  |   {x1=e1, ..., xn=en}  | #x e  |   X(e)  |  case e of p1=>e| ... | pn=>en

~0.001, foo, not b, 2 + 2Cons(2, Nil)

patterns

p ::= x  |  (p1,..., pn)  |  {x1= p1,...,xn= pn}  |  X  |  X ( p )

a:int, (x:int,y:int), I(x:int)

declarations

d ::= val p = e  |  fun y p : t = e  |  datatype Y X1 [of t1] | ... | X[of tn]

val one = 1
fun square(x: int):  int
datatype d = N | I of int

types

t ::= int  |  real  |  bool  |  string  |  char  |  t1->t2  |  t1*...*tn  |  {x1:t1x2:t2,..., xn:tn}  |  Y

int, string, int->int, bool*int->bool

values

v ::= c  |  (v1,...,vn) |  {x1=v1, ..., xn=vn}  |  X(v)

2, (2,"hello"), Cons(2,Nil)

A program is now an expression or a declaration.

A substitution is a finite map from identifiers (variables) to expressions. We represent the substitution as a list of identifiers and expressions (i.e., [(x1,e1),(x2,e2),...,(xn,en)]). We can perform a substitution S = [(x1,e1),(x2,e2),...,(xn,en)] on an expression or declaration by first substituting e1 for x1, then e2 for x2, ..., then xn for en. This is what the specification substitute does.

Rule #E7 [let]: to evaluate let d in e end, evaluate the declaration d to get a substitution S. Perform the substitution S on e yielding a new expression e'. Then evaluate e' to get the final answer.

eval(let d in e end) = v where
  (0) eval_decl(d) = S
  (1) substitute(S,e) = e'
  (2) eval(e') = v

Rule #D1[val declarations]: to evaluate a declaration val id = e, evaluate e to a value v and match v against the identifier id to yield a substitution S. The substitution S is the result of the declaration.

eval_decl(val p = e) = S where
  (0) eval(e) = v
  (1) match(v,p) = S
 
 

Top-level loop, and variable scope

Remember that to find the value of a variable we just look textually (lexically) "up" from the piece of code where it is referenced, and find the first binding occurrence. 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):int => 3 * z);
 
triple(14) ==
let val triple:int->int =  (fn(z:int):int => 3 * z) in 
  triple(14)
end

Higher-order procedures (or, where the substitution model first shows its value)

So far the substitution model looks pretty simplistic; it's a set of rules that tells you things you already know. However, understanding the substitution model is the key to prelim #1 and the first 1/3 of the course (as well as the final).

HOP's are the first example of something that is easy to understand if you really get the substitution model, and impossible otherwise.

We need one more thing in our language subset, namely fun. (fun fun…)

For the moment only, we will assume that fun is used just as a declaration, as in

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

We will assume this is shorthand for saying

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

Note that this is NOT the final story (like Newtonian physics), but a simplification for the moment. We will tell you when we change the story (relativity?)

Higher-order procedures : first examples

Functions are values just like any other value in SML. What does that mean exactly? This means that we can pass functions around as arguments to other functions, that we can store functions in data structures, that we can return functions as a result from other functions. The full implication of this will not hit you until later, but believe us, it will.

Let us look at why it is useful to have higher-order functions. The first reason is that it allows you to write more general code, hence more reusable code. As a running example, consider these functions

fun triple (z:int):int = 3 * z
fun cube (z:int):int = z * z * z

Let us now come up with a function to multiply a number by 9. We could do it directly, but for utterly twisted motives decide to use the function triple above

fun mul9 (z:int):int = triple (triple (z))

Now let's find a way to get the 9th power of a number:

fun pow9 (z:int):int = cube (cube (z))

There is a totally unexpected and unplanned similarity between these two functions: what they do is apply a given function twice to a value. By passing in the function to apply twice as an argument, we can reuse code:

fun apply_twice (f:int->int, x:int):int = f (f (x))

Using this we can now write

val x = apply_twice(triple,4)           (* 36 *)
 
fun new_mul9(z:int):int = apply_twice(triple,z)
 
val x2 = new_mul9(4)                    (* 36 *)
 
fun new_pow9(z:int):int = apply_twice(cube,z)
 
val x3 = new_pow9(4)                    (* 512 *)

I strongly recommend that those of you who wish to pass CS312 (and especially prelim #1) work out these examples by hand using the substitution model.

The advantage is that the similarity between these two functions has been made manifest. Doing this is very helpful. If someone comes up with an improved version of apply_twice, then every function that uses it profits from the improvement.

The function apply_twice is a so-called higher-order function: it is a function from functions to other values. Notice the type of apply_twice is ((int -> int) * int) -> int

In order not to pollute the top level namespace, it can be useful to locally define the function to pass in as an argument. For example:

fun fourth (x:int):int = 
  let 
    fun square (y:int):int = y * y
  in
    apply_twice (square,x)
  end

However, it seems silly to define and name a function simply to pass it in as an argument to another function. After all, all we really care about is that apply_twice get a function that double its argument. So let's do that, using new notation:

fun fourth (x:int):int = apply_twice (fn (y:int) => y*y,x)

Anonymous functions are useful for creating functions to pass as arguments to other functions, but are also useful for writing functions that return other functions! Let us revisit the apply_twice function. We now write a function twice which takes a function as an argument and return a new function that applies the original function twice:

type base = int
type func = int -> int
 
fun twice_func (f:func):func =
  fn (x:base) => f (f (x))
 
val newer_mul9 = twice_func(triple);
val newer_pow9 = twice_func(cube);
 
fun compose (f:func, g:func):func =
  fn (x:base) => f (g (x))