Bindings and scopes
- Readings
- Overview
- Binding symbols to values in SML
- Extending our expression language to build states.
- Evaluation of expressions with variables and bindings
- Evaluation with dynamic scoping
- Evaluating expressions with free variables
- Checking whether expressions are closed (no free variables)
- Generalizing the expression language for Booleans
Readings
- Pre-recorded lecture
- Chapter 10 of the textbook
- Introduction to Programming Languages entry on scope.
Overview
- Binding and scopes.
- Extending the basic expression language with let binders.
- Evaluating expressions with let binders.
- Static and dynamic scoping
- Examples.
- Finite sets in SML
- Implementing finite sets using lists.
- Free and bound variables.
- Closed expressions.
Binding symbols to values in SML
val a = 5;
a+3;
fun square x = x*x;
fun double x = x+x;
let val b = 5 in b + 3 end;
b;
Extending our expression language to build states.
We will also use a single datatype containing constants of different values and make primitive operations on these values interpretable according to the primitive. For example:
3 + 5
would bePrim("+", IConst 3, IConst 5)
x and y
would bePrim("and", Var "x", Var "y")
Thus the expression language would be:
datatype expr =
IConst of int
(* Catch-all for all binary primitive operations *)
| Prim2 of string * expr * expr
| Var of string
(* an operator to bind a variable to a value in an expression *)
| Let of string * expr * expr;
The equivalent expression for the SML expression
let val z = 17 in z + z end
would be
val e1 = Let("z", IConst 17, Prim2("+", Var "z", Var "z"));
Evaluation of expressions with variables and bindings
exception FreeVar;
fun lookup [] id = raise FreeVar
| lookup ((k:string, v)::t) id = if k = id then v else lookup t id;
fun eval (e:expr) (st: (string * expr) list) : int =
case e of
(IConst i) => i
| (Var v) => eval (lookup st v) st
| (Prim2(f, e1, e2)) =>
let
val v1 = (eval e1 st);
val v2 = (eval e2 st) in
case f of
("+") => v1 + v2
| ("-") => v1 - v2
| ("*") => v1 * v2
| ("/") => v1 div v2
| _ => raise Match
end
| (Let(x, e1, e2)) => (* i.e., let val x = e1 in e2 end *)
let val v = eval e1 st; (* evaluate e1 in the input state *)
val st' = (x, IConst v) :: st in (* update the state *)
eval e2 st' (* evaluate e2 with new state*)
end
| _ => raise Match;
e1;
eval e1 [];
Examples
- The equivalent expression and evaluation for the SML expression
let val z = 5 - 4 in 100 * z end
would be
val e2 = Let("z", Prim2("-", IConst 5, IConst 4), Prim2("*", IConst 100, Var "z")); eval e2 [];
- The equivalent expression and evaluation for the SML expression
(20 + (let val z = 17 in z + 2 end)) + 30
would be
val e3 = Prim2("+", Prim2("+", IConst 20, Let("z", IConst 17, Prim2("+", Var "z", IConst 2))), IConst 30); eval e3 [];
- The equivalent expression and evaluation for the SML expression
2 * (let val x = 3 in x + 4 end)
would be
val e4 = Prim2("*", IConst 2, Let("x", IConst 3, Prim2("+", Var "x", IConst 4))); eval e4 [];
- The equivalent expression and evaluation for the SML expression
let val z = 17 in (let val z = 22 in 100 * z end) + z end
would be
val e5 = Let("z", IConst 17, Prim2("+", Let("z", IConst 22, Prim2("*", IConst 100, Var "z")), Var "z")); eval e5 [];
- The equivalent expression and evaluation for the SML expression
let val x = 5 in let val y = 2 * x in let val x = 3 in x + y end end end
would be
val e6 = Let("x", IConst 5,
Let("y",
Prim2("*", IConst 2, Var "x"),
Let("x", IConst 3,
Prim2("+", Var "x", Var "y"))));
eval e6 [];
The evaluation of e6
is 13
because the value associated with y
is defined as soon as it is bound. This is because the scope of y
is determined statically, thus the evaluation of its value must be done eagerly. If our language had a dynamic scope on the other hand the above expression should evaluate to 9
, since the value to be used for x
in the definition of y
would be determined at the moment y
is evaluated.
Evaluation with dynamic scoping
fun eval (e:expr) (st: (string * expr) list) : int =
case e of
(IConst i) => i
| (Var v) => eval (lookup st v) st
| (Prim2(f, e1, e2)) =>
let
val v1 = (eval e1 st);
val v2 = (eval e2 st) in
case f of
("+") => v1 + v2
| ("-") => v1 - v2
| ("*") => v1 * v2
| ("/") => v1 div v2
| _ => raise Match
end
| (Let(x, e1, e2)) => eval e2 ((x, IConst (eval e1 st))::st)
(* We'd achieve dynamic scoping by replacing the above with
| (Let(x, e1, e2)) => eval e2 ((x, e1)::st)
*)
| _ => raise Match;
e6;
eval e6 [];
Evaluating expressions with free variables
To evaluate an expression with free variables we need to start the evaluation function with a valuation to these variables.
val e7 = Let("y", IConst 49, Prim2("*", Var "x", Prim2("+", IConst 3, Var "y")));
eval e7 [];
eval e7 [("x", IConst 0)];
eval e7 [("x", IConst 1)];
Checking whether expressions are closed (no free variables)
We will now determine whether expressions are closed, i.e., they do not contain free variables. Only closed expressions can be evaluated without an initial valuation.
To do this we will first define set operations in SML, then how to compute the set of free variables of an expression, then write a function that checks whether that set is empty, in which case the expression will be closed.
Finite sets in SML
A set can be implemented as a list with no duplicate elements.
- Set membership
isIn x s is true iff x occurs in s
(* *) fun isIn x s = case s of [] => false | (h::t) => if x = h then true else isIn x t;
- Set union
(union s1 s2)
takes two lists representing sets and yields a list representing their union, i.e., a list without duplicates consisting of all the elements of s1
and all the elements of s2
.
fun union s1 s2 =
case s1 of
[] => s2
| (h::t) => if isIn h s2 then union t s2 else union t (h::s2);
- Set difference
(diff s1 s2)
takes two lists representing sets and returns a list representing their difference (i.e., returns a list without duplicates consisting of all elements ofs1
that are not ins2
).
fun diff s1 s2 =
case s1 of
[] => []
| (h::t) => if isIn h s2 then diff t s2 else h::(diff t s2);
- Tests
val s1 = ["x1", "x2"];
isIn "x1" s1;
isIn "y" s1;
val s2 = ["x2", "x3"];
union s1 s2;
diff s1 s2;
Computing the set of free variables
The free variables of an expression are all the variables that are not bound.
fun freeVars e : string list =
case e of
IConst _ => []
| (Var v) => [v]
| (Prim2(_, e1, e2)) => union (freeVars e1) (freeVars e2)
| (Let(v, e1, e2)) =>
let val v1 = freeVars e1;
val v2 = freeVars e2
in
union v1 (diff v2 [v]) (* let binds x in e2 but not in e1 *)
end
| _ => raise Match;
e1;
freeVars e1;
e2;
freeVars e2;
e3;
freeVars e3;
e7;
freeVars e7;
eval e7 [];
Checking closedness
We can now define the method that checks whether an expression is closed:
fun closed e = (freeVars e = []);
closed e1;
closed e7;
We can now define a method that only evaluates closed expressions
exception NonClosed;
fun run e =
if closed e then
eval e []
else
raise NonClosed;
run e1;
run e7;
Generalizing the expression language for Booleans
datatype expr =
IConst of int
| BConst of bool
(* Catch-all for all binary primitive operations *)
| Prim2 of string * expr * expr
(* Catch-all for all unary primitive operations *)
| Prim1 of string * expr
| Ite of expr * expr * expr
| Var of string
(* an operator to bind a variable to a value in an expression *)
| Let of string * expr * expr;
fun intToBool 1 = true
| intToBool 0 = false
| intToBool _ = raise Match;
fun boolToInt true = 1
| boolToInt false = 0;
fun eval (e:expr) (st: (string * expr) list) : int =
case e of
(IConst i) => i
| (BConst b) => boolToInt b
| (Var v) => eval (lookup st v) st
| (Prim2(f, e1, e2)) =>
let
val v1 = (eval e1 st);
val v2 = (eval e2 st) in
case f of
("+") => v1 + v2
| ("-") => v1 - v2
| ("*") => v1 * v2
| ("/") => v1 div v2
| ("and") => boolToInt ((intToBool v1) andalso (intToBool v2))
| ("or") => boolToInt ((intToBool v1) orelse (intToBool v2))
| _ => raise Match
end
| (Prim1("not", e1)) => boolToInt (not (intToBool (eval e1 st)))
| (Ite(c, t, e)) => if (intToBool (eval c st)) then eval t st else eval e st
| (Let(x, e1, e2)) => eval e2 ((x, IConst (eval e1 st))::st)
(* We'd achieve dynamic scoping by replacing the above with
| (Let(x, e1, e2)) => eval e2 ((x, e1)::st)
*)
| _ => raise Match;