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.
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…
|
syntactic class |
syntactic variable(s) and grammar
rule(s) |
examples |
|
constants |
c |
... |
|
unary operator |
u |
|
|
binary operators |
b |
|
|
expressions (terms) |
e ::= c | u e | e1 b e2 | |
|
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)
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 |
|
|
constants |
c |
... |
|
unary operator |
u |
|
|
binary operators |
b |
|
|
expressions (terms) |
e ::= x | c | u e | e1 b e2 | |
|
|
types |
t ::= |
|
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.
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
Having briefly introduced let, we can now turn our attention to the issue of what it means to substitute a value for a variable.
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: realoperand: intin expression: square_root 9
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:
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. (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. 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.
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 valx:int = 1
fun f(x:int) = x
valy:int = x+1
infn(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
valy:int = 1+1
infn(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:
letvalx:t = vineend -->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:
letvalp = vineend -->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.
letd1...dnineend -->
letd1in letd2...dnineend 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}
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 |
x, y |
|
|
datatypes, datatype
constructors |
X, Y |
|
|
constants |
c |
... |
|
unary operator |
u |
|
|
binary operators |
b |
|
|
expressions (terms) |
e ::= c | x | u e | e1 b e2 | |
|
|
patterns |
p ::= c | x | |
|
|
declarations |
d ::= |
|
|
types |
t ::= |
|
|
values |
v ::= c | |
|
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
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 Eend
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
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?)
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 * zfun 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 = inttype 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))