Roadmap
Past three weeks:
- How do we use a functional language?
Next three weeks:
- How do we implement a functional language?
- … in a functional language (of course)
This week: Interpreter
- How do we evaluate a program given its abstract syntax tree (AST)?
- How do we prove properties about our interpreter (e.g. that certain programs never crash)?
The Nano Language
Features of Nano:
- Arithmetic
- Variables
- Let-bindings
- Functions
- Recursion
1. Nano: Arithmetic
A grammar of arithmetic expressions:
::= n
e | e1 + e2
| e1 - e2
| e1 * e2
Examples:
Expression | Value | |
---|---|---|
4 |
==> |
4 |
4 + 12 |
==> |
16 |
(4 + 12) - 5 |
==> |
11 |
Representing Expressions and Values
Let’s represent arithmetic expressions as a type:
data Expr = Num Int
| Add Expr Expr
| Sub Expr Expr
| Mul Expr Expr
Let’s represent arithmetic values as a type:
type Value = Int
Evaluating Arithmetic Expressions
We can now write a Haskell function to evaluate an expression:
eval :: Expr -> Value
Num n) = n
eval (Add e1 e2) = eval e1 + eval e2
eval (Sub e1 e2) = eval e1 - eval e2
eval (Mul e1 e2) = eval e1 * eval e2 eval (
Alternative representation
Let’s factor out the operators into a separate type
data Binop = Add | Sub | Mul
data Expr = Num Int -- number
| Bin Binop Expr Expr -- binary expression
QUIZ
Evaluator for alternative representation:
eval :: Expr -> Value
Num n) = n
eval (Bin op e1 e2) = evalOp op (eval e1) (eval e2) eval (
What’s a suitable type for evalOp
?
(A) Binop -> Value
(B) Binop -> Value -> Value -> Value
(C) Binop -> Expr -> Expr -> Value
(D) Binop -> Expr -> Expr -> Expr
(E) Binop -> Expr -> Value
Answer: B
The Nano Language
Features of Nano:
- Arithmetic [done]
- Variables
- Let-bindings
- Functions
- Recursion
2. Nano: Variables
Let’s add variables!
::= n -- OLD
e | e1 + e2
| e1 - e2
| e1 * e2
| x -- NEW
Let’s extend the representation of expressions:
data Expr = Num Int -- number
| Bin Binop Expr Expr -- binary expression
| ??? -- variable
type Id = String
data Expr = Num Int -- number
| Bin Binop Expr Expr -- binary expression
| Var Id -- variable
Now let’s extend the evaluation function!
QUIZ
What should the following expression evaluate to?
+ 1 x
(A) 0
(B) 1
(C) Runtime error
Answer: C
Environment
An expression is evaluated in an environment
- It’s a dictionary that maps variables to values
"x" := 0, "y" := 12, ...] [
We can represent an environment using the following type:
type Env = [(Id, Value)]
Evaluation in an Environment
We write
==> value eval env expr
To mean that evaluating expr
in the environment env
returns value
QUIZ
What should the result of?
"x" := 0, "y" := 12, ...] (x + 1) eval [
(A) 0
(B) 1
(C) Runtime error
Answer: B
To evaluate a variable, look up its value in the environment!
Environment | Expression | Value | |
---|---|---|---|
["x" := 5] |
x |
==> |
5 |
["x" := 5] |
x + 12 |
==> |
17 |
["x" := 5] |
y - 5 |
==> |
error |
Evaluating Variables
We need to update our evaluation function to take the environment as an argument:
eval :: Env -> Expr -> Value
Num n) = ???
eval env (Bin op e1 e2) = ???
eval env (Var x) = ??? eval env (
eval :: Env -> Expr -> Value
Num n) = n
eval env (Bin op e1 e2) = evalOp op (eval env e1) (eval env e2)
eval env (Var x) = lookup x env eval env (
But how do variables get into the environment?
The Nano Language
Features of Nano:
- Arithmetic [done]
- Variables [done]
- Let-bindings
- Functions
- Recursion
3. Nano: Let Bindings
Let’s add let bindings!
::= n -- OLD
e | e1 + e2
| e1 - e2
| e1 * e2
| x
| let x = e1 in e2 -- NEW
Example:
Environment | Expression | Value | |
---|---|---|---|
[] |
let x = 2 + 3 in x * 2 |
==> |
10 |
Let’s extend the representation of expressions:
data Expr = Num Int -- number
| Bin Binop Expr Expr -- binary expression
| Var x -- variable
| ??? -- let binding
data Expr = Num Int -- number
| Bin Binop Expr Expr -- binary expression
| Var Id -- variable
| Let Id Expr Expr -- let binding
Now let’s extend the evaluation function!
eval :: Env -> Expr -> Value
Num n) = n
eval env (Bin op e1 e2) = evalOp op (eval env e1) (eval env e2)
eval env (Var x) = lookup x env
eval env (Let x def body) = ??? eval env (
Let’s develop intuition with examples!
QUIZ
What should this evaluate to?
let x = 2 + 3
in
+ 1 x
(A) 1
(B) 5
(C) 6
(D) Error: unbound variable x
(E) Error: unbound variable y
Answer: C
QUIZ
What should this evaluate to?
let x = 5
in
let y = x + 1
in
* y x
(A) 5
(B) 6
(C) 30
(D) Error: unbound variable x
(E) Error: unbound variable y
Answer: C
QUIZ
What should this evaluate to?
let x = 0
in
let x = 100
(in
+ 1
x + x )
(A) 1
(B) 101
(C) 201
(D) 2
(E) Error: multiple definitions of x
Answer: B
Principle: Static (Lexical) Scoping
Every variable use (occurrence) gets its value from the most local definition (binding)
- in a pure language, the value never changes once defined
- easy to tell by looking at the program, where a variable’s value came from!
Implementing Lexical Scoping
Example 1:
-- environment:
let x = 2 + 3 -- []
in -- [x := 5]
+ 1 -- | x
Note: first evaluate 2 + 3
and bind x
to the resulting value in the environment
Example 2:
-- environment:
let x = 5 -- []
in -- [x := 5]
let y = x + 1 -- |
in -- | [y := 6, x := 5]
* y -- | | x
Note: [y := 6]
got added to the environment
Example 3:
-- environment:
let x = 0 -- []
in -- [x := 0]
let x = 100 -- |
(in -- | [x := 100, x := 0]
+ 1 -- | |
x -- | |
) + x -- |
Note: [x := 100]
was only added for the inner scope
Evaluating let Expressions
To evaluate let x = e1 in e2
in env
:
- Evaluate
e1
inenv
toval
- Extend
env
with a mapping["x" := val]
- Evaluate
e2
in this extended environment
eval :: Env -> Expr -> Value
Num n) = n
eval env (Bin op e1 e2) = evalOp op (eval e1) (eval e2)
eval env (Var x) = lookup x env
eval env (Let x e1 e2) = eval env' e2
eval env (where
= eval env e1
v = (x, v) : env env'
The Nano Language
Features of Nano:
- Arithmetic [done]
- Variables [done]
- Let binding [done]
- Functions
- Recursion
4. Nano: Functions
Let’s add:
- lambda abstraction (aka function definitions)
- applications (aka function calls)
::= n -- OLD
e | e1 + e2
| e1 - e2
| e1 * e2
| x
| let x = e1 in e2
-- NEW
| \x -> e -- abstraction
| e1 e2 -- application
Example:
let inc = \x -> x + 1 in
10 inc
QUIZ
What should this evaluate to?
let inc = \x -> x + 1 in
10 inc
(A) Undefined variable x
(B) Undefined variable inc
(C) 1
(D) 10
(E) 11
Answer: E
Representing functions
Let’s extend the representation of expressions:
data Expr = Num Int -- number
| Bin Binop Expr Expr -- binary expression
| Var Id -- variable
| Let Id Expr Expr -- let expression
| ??? -- abstraction
| ??? -- application
data Expr = Num Int -- number
| Bin Binop Expr Expr -- binary expression
| Var Id -- variable
| Let Id Expr Expr -- let expression
| Lam Id Expr -- abstraction: formal + body
| App Expr Expr -- application: function + actual
Example:
let inc = \x -> x + 1 in
10 inc
represented as:
Let "inc"
Lam "x" (Bin Add (Var "x") (Num 1)))
(App (Var "inc") (Num 10)) (
Evaluating Functions
-- environment
let inc = \x -> x + 1
in -- [inc := ???]
10 -- use the value of inc to evaluate this inc
What is the value of inc
???
Rethinking our values
Until now: a program evaluates to an integer (or fails)
type Value = Int
type Env = [(Id, Value)]
eval :: Env -> Expr -> Value
What do these programs evaluate to?
1)
(-> x + 1
\x ==> ???
2)
(let f = \x y -> x + y in
1
f ==> ???
Conceptually, they both evaluate to a function that increments its argument
Now: a program evaluates to an integer or a function (or fails)
- Remember: functions are first-class values
Let’s change our definition of values!
data Value = VNum Int
| VFun ??? -- What info do we need to store?
-- Other types stay the same
type Env = [(Id, Value)]
eval :: Env -> Expr -> Value
Function values
How should we represent a function value?
let inc = \x -> x + 1 in
10 inc
We need to store enough information about inc
so that we can later evaluate any application of inc
(like inc 0
, inc 5
, inc 10
, inc (factorial 100)
)
The value of a function is its code!
Representing Function Values (First Attempt)
Grammar for values:
::= n -- OLD: number
v | <x, e> -- NEW: formal + body
Haskell representation:
data Value = VNum Int
| VFun Id Expr -- formal + body
Let’s try this!
-- environment
let inc = \x -> x + 1
in -- [inc := <x, x + 1>]
10 -- how do we evaluate this? inc
Evaluating applications
-- environment
let inc = \x -> x + 1
in -- [inc := <x, x + 1>]
10 -- how do we evaluate this? inc
To evaluate inc 10
:
- Evaluate
inc
, get<x, x + 1>
- Evaluate
10
, get10
- Evaluate
x + 1
in an environment extended with[x := 10]
Let’s extend our eval
function!
eval :: Env -> Expr -> Value
Num n) = ???
eval env (Bin op e1 e2) = ???
eval env (Var x) = ???
eval env (Let x e1 e2) = ???
eval env (Lam x e) = ???
eval env (App e1 e2) = ??? eval env (
eval :: Env -> Expr -> Value
Num n) = VNum n
eval env (Var x) = lookup x env
eval env (Bin op e1 e2) = evalOp op (eval env e1) (eval env e1)
eval env (Let x e1 e2) = eval env' e2
eval env (where
= eval env e1
v = (x, v) : env
env' Lam x body) = VFun x body
eval env (App fun arg) = evalApp env (eval env e1) (eval env e1)
eval env (
evalApp :: Env -> Value -> Value -> Value
VFun x body) vArg = eval env' body
evalApp env (where
= (x, vArg) : env
env' = error "applying a non-function" evalApp _ _ _
QUIZ
What should this evaluate to?
let c = 1
in
let inc = \x -> x + c
in
10 inc
(A) Undefined variable x
(B) Undefined variable c
(C) 1
(D) 10
(E) 11
Answer: E
QUIZ
And what should this evaluate to?
let c = 1
in
let inc = \x -> x + c
in
let c = 100
in
10 inc
(A) Error: multiple definitions of c
(B) 11
(C) 110
Answer: B
Reminder: Referential Transparency
The same expression must always evaluate to the same value
- In particular: a function must always return the same output for a given input
Why?
> myFunc 10
11
> myFunc 10
110
Oh no! How do I find the bug???
- Is it in
myFunc
? - Is it in a global variable?
- Is it in a library somewhere else?
My worst debugging nightmare!
Static vs Dynamic Scoping
What we want:
let c = 1 -- <-------------------
in -- \
let inc = \x -> x + c -- refers to this def \
in
let c = 100
in
10
inc
==> 11
Lexical (or static) scoping:
- each occurrence of a variable refers to the most recent binding in the program text
- definition of each variable is unique and known statically
- guarantees referential transparency:
let c = 1 -- <-------------------
in -- \
let inc = \x -> x + c -- refers to this def \
in
let c = 100
in
let res1 = inc 10 -- ==> 11
in
let c = 200
in
let res2 = inc 10 -- ==> 11
in res1 == res2 -- ==> True
What we don’t want:
let c = 1
in
let inc = \x -> x + c -- refers to this def \
in -- \
let c = 100 -- <-------------------
in
10
inc
==> 110
Dynamic scoping:
- each occurrence of a variable refers to the most recent binding during program execution
- can’t tell where a variable is defined just by looking at the function body
- violates referential transparency:
let c = 1
in
let inc = \x -> x + c -- refers to this def \ \
in -- \ \
let c = 100 -- <------------------- \
in -- \
let res1 = inc 10 -- ==> 110 \
in -- \
let c = 200 -- <----------------------
in
let res2 = inc 10 -- ==> 210!!!
in res1 == res2 -- ==> False
QUIZ
Which scoping does our eval
function implement?
...
Lam x body) = VFun x body
eval env (App fun arg) = evalApp env (eval env e1) (eval env e1)
eval env (where
VFun x body) vArg = eval ((x,vArg):env) body evalApp env (
(A) Static
(B) Dynamic
(C) Neither
Answer: B
Let’s find out!
-- env:
let c = 1 -- []
in -- ["c" := 1]
let inc = \x -> x + c
in -- ["inc" := <x, x + c>, "c" := 1]
let c = 100
in -- ["c" := 100, "inc" := <x, x + c>, "c" := 1]
10
inc
-- 1. ==> <x, x + c>
-- 2. ==> 10
-- 3. x + c ["x" := 10, "c" := 100, "inc" := <x, x + c>, "c" := 1]
-- ==> 110
Ouch.
What went wrong?
let c = 1
in -- ["c" := 1]
let inc = \x -> x + c -- we want this "c" to ALWAYS mean 1!
in -- ["inc" := <x, x + c>, "c" := 1]
let c = 100
in -- ["c" := 100, "inc" := <x, x + c>, "c" := 1]
10 -- but now it means 100 because we are in a new env! inc
Lesson learned: need to remember what c
was bound to when inc
was defined!
- i.e. “freeze” the environment at the point of function definition
Implementing Static Scoping
Key ideas:
- At definition: Freeze the environment in the function’s value
- At call: Use the frozen environment to evaluate the body
- instead of the current environment
-- env:
let c = 1 -- []
in -- ["c" := 1]
let inc = \x -> x + c
in -- ["inc" := <fro, x, x + c>, "c" := 1]
-- where fro = ["c" := 1]
let c = 100
in -- ["c" := 100, "inc" := <fro, x, x + c>, ...]
10
inc
-- 1. ==> <fro, x, x + c>
-- 2. ==> 10
-- add "x" to fro instead of env:
-- 3. x + c ["x" := 10, "c" := 1]
-- ==> 11
Tada!
Function Values as Closures
To implement lexical scoping, we will represent function values as closures
Closure = lambda abstraction (formal + body) + environment at function definition
Updated grammar for values:
::= n
v | <env, x, e> -- NEW: frozen env + formal + body
::= []
env | (x := v) : env
Updated Haskell representation:
data Value = VNum Int
| VClos Env Id Expr -- frozen env + formal + body
Evaluating function definitions
How should we modify our eval
for Lam
?
data Value = VNum Int
| VClos Env Id Expr -- env + formal + body
eval :: Env -> Expr -> Value
Lam x body) = ??? -- construct a closure eval env (
Recall: At definition: Freeze the environment in the function’s value
Exact code for you to figure out in HW4
Evaluating function calls
How should we modify our eval
for App
?
data Value = VNum Int
| VClos Env Id Expr -- env + formal + body
eval :: Env -> Expr -> Value
App e1 e2) = ??? -- apply the closure eval env (
Recall: At call: Use the frozen environment to evaluate the body
Exact code for you to figure out in HW4
Hint: Recall evaluating inc 10
:
- Evaluate
inc
to get<fro, x, x + c>
- Evaluate
10
to get10
- Evaluate
x + c
in(x := 10) : fro
Let’s generalize to e1 e2
:
- Evaluate
e1
to get<fro, param, body>
- Evaluate
e2
to getv2
- Evaluate
body
in(param := v2) : fro
Advanced Features of Functions
- Functions returning functions
- aka partial applications
- Functions taking functions as arguments
- aka higher-order functions
- Recursion
Does our eval
support this?
QUIZ
What should the following evaluate to?
let add = \x y -> x + y
in
let add1 = add 1
in
let add10 = add 10
in
100 + add10 1000 add1
(A) Runtime error
(B) 1102
(C) 1120
(D) 1111
Answer: D
Partial Applications Achieved!
Closures support functions returning functions!
let add = \x -> (\y -> x + y) -- env0 = []
in -- env1 = ["add" := <[], x, \y -> x + y>]
let add1 =
1 -- eval ("x" := 1 : env0) (\y -> x + y)
add -- ==> <["x" := 1], y, x + y>
in -- env2 = ["add1" := <["x" := 1], y, x + y>, "add" := ...]
let add10 =
10 -- eval ("x" := 10 : env0) (\y -> x + y)
add -- ==> <["x" := 10], y, x + y>
in -- env3 = ["add10" := <["x" := 10], y, x + y>, "add1" := ...]
100 -- eval ["y" := 100, "x" := 1] (x + y)
add1 -- ==> 101
+
1000 -- eval ["y" := 1000, "x" := 10] (x + y)
add10 -- ==> 1010
==> 1111
QUIZ
What should the following evaluate to?
let inc = \x -> x + 1
in
let doTwice = \f -> (\x -> f (f x))
in
10 doTwice inc
(A) Runtime error
(B) 11
(C) 12
Answer: C
Higher-order Functions Achieved
Closures support functions taking functions as arguments!
let inc = \x -> x + 1 -- env0 = []
in -- env1 = [inc := <[], x, x + 1>]
let doTwice = \f -> (\x -> f (f x))
in -- env2 = [doTwice := <env1, f, \x -> f (f x)>, inc := ...]
-- eval ("f" := <[],x,x + 1> : env1) (\x -> f (f x))
((doTwice inc) -- ==> <("f" := <[],x,x + 1> : env1), x, f (f x)>
10) -- eval ["x" := 10, "f" := <[],x,x + 1>, ...] f (f x)
-- f ==> <[], x, x + 1>
-- x ==> 10
-- <[], x, x + 1> 10 ==> eval ["x" := 10] x + 1 ==> 11
-- <[], x, x + 1> 11 ==> eval ["x" := 11] x + 1 ==> 12
==> 12
QUIZ
What does this evaluate to?
let f = \n -> n * f (n - 1)
in
5 f
(A) 120
(B) Evaluation does not terminate
(C) Error: unbound variable f
Answer: C
let f = \n -> n * f (n - 1)
in -- [f := <[], n, n * f (n - 1)>]
5 -- eval [n := 5] (n * f (n - 1))
f -- ==> unbound variable f!!!
Lesson learned: to support recursion, you need to put the function itself back into its closure environment before the body gets evaluated!
The Nano Language
Features of Nano:
- Arithmetic [done]
- Variables [done]
- Let bindings [done]
- Functions [done]
- Recursion [you figure it out in HW4]
That’s all folks!