Administrivia: PS#3 out today, pick a partner (or not) by next Friday.
Last time: evaluation model and rewrite rules.
This time: a slightly more program-like definition of the evaluation model. There will be a handout available on the web shortly (by tomorrow).
We will still be looking at a subset of ML, consisting of
+,*,=
if
let
(not strictly necessary, but very convenient)
fn
but we will soon add fun as well.
We can formally define the syntax of our ML subset:
Expressions:
e ::= c (* constants *) | id (* variables *) | (fn (id:t) => 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 *) [ADD LATER] | (fun id(id1:t1):t2 = e) (* recursive functions *) [ADD LATEST OF ALL]
Declarations:
d ::= val id = e (* value declarations *) [ADD LATER] | fun id(id1:t1):t2 = e (* function declarations *) [ADD EVEN LATER]
Values:
v ::= c (* constant values *) | (fn (id:t):t' => e) (* anonymous functions *)
We will now specify a function called eval. This is not an ML function (yet!). Instead it is a recursive specification (abstract description) of the result of evaluating any ML function. For each element of the syntax we have a rule. Here are the ones corresponding to what we did in lecture on Tuesday:
Rule #E1 [constants]: constants evaluate to themselves
eval(c) = c
Rule #E2 [functions]: anonymous functions evaluate to themselves
eval(fn (id:t) => e) = (fn (id:t) => e)
Rule #E3 [function calls]: 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'
Rule #E4 [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 #E5 [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 #E6 [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)
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.
Now we can do our earlier examples slightly more precisely. The only real difference from the re-write rules we described earlier is that now we are specifying the order of evaluation; so for example we need to evaluate a function's arguments before we call it.
Examples:
(fn(y:int) => y*y*y)(2+1) (if (1 < 2) then (fn(y:int) => y*y*y) else (fn(y:int) => y*y))(~ 4) (fn(y:int) => (fn(z:int) => z*y))(2+1) let val x:int = 2+1 in (* turn this into function application *) fn(z:int) => z+x end
LET
and variable declarationsWe can also add LET
to the language as a primitive, which is useful (though not, strictly speaking, necessary) (just like having *
is not necessary if you have +
).
[Add syntax rules for let and declaration]
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 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
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 * 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 9
th power of a number:
fun pow9 (z:int):int = cube (cube (z))
There is a totally unexpected and unplanned similarity Using this we can now write
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 The function 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:
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 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:
fun apply_twice (f:int->int, x:int):int = f (f (x))
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 *)
apply_twice
, then every function that uses it profits from the improvement.
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
fun fourth (x:int):int =
let
fun square (y:int):int = y * y
in
apply_twice (square,x)
end
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)
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))