Final Exam
Monday, December 9
10:30-12:30 AM
Open-book, open-notes, no computers
Review and sample questions
===========================
Notes:
Everything on the mid-term (and in the mid-term review)
still applies.
">" in left--hand column marks sample questions.
Recursive data definitions and recursive functions
--------------------------------------------------------
; A foobar is
; * 7, or
; * (list n f 9)
; where n is a number and f is a foobar
= 7
| (list 9)
> Which of these expressions are s?
* 7
* (list 0 7 9)
* (list 9 7 0)
* (list 0 (list 0 (list 0 (list 0 7 9) 9) 9) 9)
> Provide a proof tree showing that (list 1 (list 7 7 9) 9) is a
.
7 is in 7 is in
--------------------------------
(list 7 7 9) is in 1 is in
--------------------------------------------------
(list 1 (list 7 7 9) 9) is in
[The answer MUST have the above shape.]
Functions that consume s:
; F : foobar -> ...
(define (F fb)
(cond
[(eq? fb 7) ...]
[else ... (car fb)
... (F (cadr fb))
...]))
- F must have a "cond" expression
- The "cond" expression must have two cases
- The first case must recognize an instance
of the first case in the data definition
- Each case should extract (non-constant) members
of fb
- Data members of type will likely be passed
to F
> Implement "sum", which takes a and returns
the sum of all numbers in the .
; sum : foobar -> num < CONTRACT
; Adds up all the numbers in fb. < PURPOSE
; (sum 7) = 7 < EXAMPLES
; (sum (list 12 7 9)) = 28 (one for each case)
(define (sum fb) < IMPLEMENTATION
(cond (follows shape)
[(eq? fb 7) 7]
[else (+ (car fb) (sum (cadr fb)) 9)]))
Mutually recursive data definitions imply mutually
recursive procedures:
= 1
| (cons 2 **)
**** = (cons **** )
; Fa : a -> ...
(define (Fa a)
(cond
[(eq? a 1) ...]
[else ... (Fb (cdr a)) ...]))
; Fb : b -> ...
(define (Fb b)
; Only one case:
... (Fa (car b))
... (Fa (cdr b))
...)
> Implement the function "sum", which takes an
and returns the sum of all numbers in the .
... like above; needs CONTRACT, PURPOSE,
and EXAMPLES ...
Scheme Reductions
---------------------------------------------------------
Using the following subset of Scheme:
=
| (let ( ) )
| (lambda () )
| ( )
= (lambda () )
> Show the evaluation of
(let (f (lambda (x) x)) ((f f) (lambda (y) y)))
to a value:
(let (f (lambda (x) x)) ((f f) (lambda (y) y)))
-> (((lambda (x) x) (lambda (x) x)) (lambda (y) y))
-> ((lambda (x) x) (lambda (y) y))
-> (lambda (y) y)
Lexical scope
---------------------------------------------------------
Assuming our proc language, with `let' and `letrec':
- Free variables:
x is free in E if
* E = y and
x = y
* E = proc(y1, ...)E2 and
y1 != x AND ... AND x is free in E2
* E = (E1 E2) and
x is free in E1 OR x is free in E2
* E = prim(E1, ..) and
x is free in E1 OR ...
* E = let y1 = E1 ... in Eb and
x is free in E1 OR ...
OR y1 != x AND ... AND x is free in Eb
* E = letrec y1 = E1 ... in Eb and
y1 != x AND ... AND
x is free in E1 OR ... OR x is free in Eb
* E = if E1 then E2 else E3 and
x is free in E1 OR ...
- Bound variables:
x is bound in E if
* E = proc(y1, ...)E2 and
y1 = x OR ... AND x is free in E2
OR x is bound in E2
* E = (E1 E2) and
x is bound in E1 OR x is bound in E2
* E = prim(E1, ..) and
x is bound in E1 OR ...
* E = let y1 = E1 ... in Eb and
x is bound in E1 OR ...x is bound in Eb
OR y1 = x OR ... AND x is free in Eb
* E = letrec y1 = E1 ... in Eb and
x is bound in E1 OR ... OR x is bound in Eb
OR y1 = x OR ... AND
x is free in E1 OR ... OR x is free in Eb
* E = if E1 then E2 else E3 and
x is bound in E1 OR ...
A _binding occurrence_ is a variable in a
"proc" argument list or a "let"/"letrec" left-hand
side which causes a variable to be bound.
> Given the expression
let g = proc(n)+(n,6)
y = g
in let x = r
y = g
in letrec f = proc(z)(f -(z,x))
in (f (y 9))
Draw arrows from bound variable to binding
occurrences.
.------------------.
v v---. |
let g = proc(n)+(n,6) |
y = g .-----'
in let x = r <-/---------------.
,---> y = g -' v------. \
/ in letrec f = proc(z)(f -(z, x))
\ ,^-----------'
`----------|--.
in (f (y 9))
> List the free vars: g, r
> List the bound vars: g, n, x, y, f, z
Variable names are interchangeable, as long as
a binding occurrence and its bound references are
changed consistently.
Renaming every binding instance in the above program
to a new, unique name, and update all bound occurrences
to produce an equivalent program:
let g1 = proc(n1)+(n1,6)
y1 = g
in let x2 = r
y2 = g1
in letrec f3 = proc(z4)(f3 -(z4,x2))
in (f3 (y2 9))
In fact, we can eliminate name at references
altogether by using lexical addresses:
@(1, 3)
^ ^--- means "the 4th binding in the set"
`--- means "cross two contours"
> Show the same program as above using lexical addresses:
let g1 = proc(n1)+(@(0,0),6)
y1 = g
in let x2 = r
y2 = @(0,0)
in letrec f3 = proc(z4)(@(1,0) -(@(0,0),@(2,0)))
in (@(0,0) (@(1,1) 9))
Interpreters for a functional language
---------------------------------------------------------
+ Lexically scoped languages
The language grammar from above:
=
| (let ( ) )
| (lambda () )
| ( )
An interepreter takes an (or its representation
in abstract syntax) and produces the result of
evaluating the expression.
An interpreter's "eval" is just another recursive
function.
(define (eval e)
(cond
[(symbol? e) ...]
[(let? e) ... (eval (cadr (cadar e)))
... (eval (caddr e))
...]
[(lambda? e) ... (eval (caddr e)) ...] ; << !
[else ... (eval (car e))
... (eval (cadr e))
...]))
It turns out that we don't want the recursive call
for the `lambda?' case, though:
(define (eval e)
(cond
[(symbol? e) ...]
[(let? e) ... (eval (cadr (cadar e)))
... (eval (caddr e))
...]
[(lambda? e) ... (closure (cadar e)
(caddr e)) ...]
[else ... (eval (car e))
... (eval (cadr e))
...]))
To implement lexical scope, the "eval" function
typically accumulates information in an environment:
(define (eval e env)
(cond
[(symbol? e) (lookup env e)]
[(let? e) (let ([v (eval (cadr (cadar e)) env)])
(eval (caddr e)
(extend env
(car (cadar e)) v)))]
[(lambda? e) (closure (cadar e)
(caddr e)
env)]
[else ... (eval (car e) env)
... (eval (cadr e) env)
...]))
The application part expects a closure as the
result of the first expression:
(define (eval e env)
(cond
[(symbol? e) (lookup env e)]
[(let? e) (let ([v (eval (cadr (cadar e)) env)])
(eval (caddr e)
(extend env
(car (cadar e)) v)))]
[(lambda? e) (closure (cadar e)
(caddr e)
env)]
[else (let ([f (eval (car e) env)]
[a (eval (cadr e) env)])
(apply f a))])
(define (apply f a)
(let ([id (closure->id f)]
[body (closure->body f)]
[env (closure->env f)])
(eval body (extend env id a))))
> Given the expression:
(let (f (lambda (y) y))
(lambda (x) f))
- Describe the closure bound to f at the point where
"(lambda (x) f)" is the current expression:
^ ^ ^--- env
| `--- body expr
`--- formal argument
- Describe the environment at the point where
"(lambda (x) f)" is the current expression:
{ f = , {} }
(The final will more likely ask such a question in terms of the
proc language.)
(let (f (lambda (y) y))
(f (lambda (x) x)))
What does the environmentlook like when then current
expression is `y'?
{ y = , {}} > , {} }
+ Assignment
In the basic language, the environment maps
variables to values.
- Expresed values: results returned by an expresion
- Denoted values: meaning of a variable
In other words, in the basic language, the set
of expressed and denoted values is the same:
Expressed values = procedures [, numbers, booleans]
Denoted values = procedures [, numbers, booleans]
When we add assignment --- e.g., set x = +(1,2) ---
then the environment maps variables to *locations*
and a location contains a value that can be replaced
with a different one.
Expressed values = procedures [, numbers, booleans]
Denoted values = locations (holding expressed vals)
+ Parameter-passing
Assignment exposes the order of evaluation.
- call-by-value: arguments evaluated before
procedure bodies
- call-by-name: expression for an argument
evaluated at the point where
the argument is used
- call-by-need: like call-by-name, but remember
the result, in case the variable
is used again
Implementation technique for call-by-name and
call-by-need: thunks = expr + env.
> Given the following expression
let x = 10
in let f = proc(y, z) { set x = +(y, y); z }
in (f { set x = +(x, 1); x } +(x, 1))
what is its value in
* call-by-value? 12
* call-by-name? 24
* call-by-need? 23
+ Continuations
There's more to evaluation order than just arguments:
how do recursive evaluations of sub-expression continue
with the rest of the evaluation?
A continuation, like a to-do list, exposes the staging
of evaluation needed to recur on subexpressions.
The "eval" function takes three arguments:
expression, envrionment, and continuation.
When it arrives at a value, it applies the
continaution.
When it starts evaluations a subexpression, it extends
the continuation.
(define (eval e env cont)
(cond
[(symbol? e) (apply-cont cont (lookup env e))]
[(let? e)
(eval (cadr (cadar e)) env
(let-cont (cadar e) env (caddr e) cont))]
[(lambda? e) (apply-cont cont
(closure (cadar e)
(caddr e)
env))]
[else
(eval (car e) env (apparg-cont
(cadr e) env
cont))]))
(define (apply-cont v cont)
(cond
[(done-cont? cont) v]
[(let-cont? cont)
(eval (let-cont->exp cont)
(extend (let-cont->env cont)
(let-cont->id cont)
v)
(let-cont->oldcont cont))]
[(apparg-cont? cont)
(eval (apparg-cont->argexp cont)
(apparg-cont->env cont)
(app-cont
v
(apparg-cont->oldcont cont)))]
[(app-cont? cont)
(apply (app-cont->proc cont)
v
(app-cont->oldcont cont))]))
(define (apply f a cont)
(let ([id (closure->id f)]
[body (closure->body f)]
[env (closure->env f)])
(eval body (extend env id a) cont)))
At this point, every call to "eval" or "apply-cont"
or "apply" produces the final result, so no context
is hidden in the Scheme implementation.
> By showing every call to "eval" and "apply-cont", show completely
how the following expresion is evaluated:
(let (f (lambda (x) x)) ((f f) (lambda (y) y)))
expr= (let (f (lambda (x) x)) ((f f) (lambda (y) y)))
env= {}
cont= [done]
expr= (lambda (x) x)
env= {}
cont [let f ((f f) (lambda (y) y)) {} [done]]
val=
cont= [let f ((f f) (lambda (y) y)) {} [done]]
expr= ((f f) (lambda (y) y))
env= {f=, {}}
cont= [done]
expr= (f f)
env= {f=, {}}
cont= [apparg (lambda (y) y) {f=, {}} [done]]
expr= f
env= {f=, {}}
cont= [apparg f {f=, {}}
[apparg (lambda (y)y) {f=, {}} [done]]]
val=
cont= [apparg f {f=, {}}
[apparg (lambda (y)y) {f=, {}} [done]]]
expr= f
env= {f=, {}}
cont= [app
[apparg (lambda (y)y) {f=, {}} [done]]]
val=
cont= [app
[apparg (lambda (y)y) {f=, {}} [done]]]
expr= x
env= {x=, {}}
cont= [apparg (lambda (y)y) {f=, {}} [done]]
val=
cont= [apparg (lambda (y)y) {f=, {}} [done]]
expr= (lambda (y) y)
env= {f=, {}}
cont= [app [done]]
val= , {}}>
cont= [app [done]]
expr= x
env= {x=, {}}>, {}}
cont= [done]
val= , {}}>
cont= [done]
+ Garbage collection
Every closure creation, environment extension, or
continuation extension must allocate memory.
That memory is never explicitly freed.
A garbage collector inspects the heap, discovers
records that certainly won't be used in the future,
and frees them.
A two-space collector frees records implcitly
by copying all the records that need to be kept to
a new space. Since it moves records one-by-one
to the first free spot in the new space, it also
defragments memory.
> Suppose an interepreter needs the following kinds
of records managed by a two-space collector:
tag 1: an integer
tag 2: two pointers
It has three registers and a main memory of
size 32, so that to-space is of size 16.
Here is the state of the machine just before
a garbage collection:
Reg 1: 4
Reg 2: 6
Reg 3: 14
1 3 1 5 1 7 2 0 4 2 2 0 1 8 1 19 <- Content
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <- Address
^ ^ ^ ^ ^ ^ ^ <- Records
7 0 2 5 <- Forwards
Show the registers and new to-space after a collection.
1 7 2 7 0 1 19 1 3
Reg 1: 0
Reg 2: 2
Reg 3: 5
1 7 2 7 0 1 19 1 3 0 0 0 0 0 0 0 <- Content
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <- Address
^ ^ ^ ^
4 6 14 0 <- Was
Types
---------------------------------------------------------
The goal of a type system is to reject programs
that the interpreter doesn't want to deal with.
Such programs include uses of "if" that use
a number for the test instead of a boolean, or
function applications that try to use a number as
a function.
For a practical language, the type system will have
to reject programs that work, as well as programs
that don't. That's an unavoidable cost of type
checking.
The benefit is that a programmer (and a compiler)
know that certain things can't happen at run-time.
Since the goal of type-checking is to prove that
certain things never happen, type judgements must
be carefully and completely justified --- even to
the point of mentioning that "4" obviously has type
"int".
Type rules:
G |- : int for any , any env
G |- false : bool
G |- true : bool
G |- x : T if x:T is in G
G |- E1 : bool G |- E2 : T G |- E3 : T
-------------------------------------------
G |- if E1 then E2 else E3 : T
(for the some T)
G, x:T |- E : T'
------------------------------------
G | - proc(T x)E : (T -> T')
G |- E1 : (T -> T') G |- E2 : T
-------------------------------------
G |- (E1 E2) : T'
> Find and a type for the following expression (where "i" abbreviates
int" and "b" abbreivates "bool"), and provide a proof tree
demonstrating the type:
proc((i -> b) x)if (x 10) then x else proc(i y)false
{x:(i -> b), y:i} |- false : b
________________________________________
G1 |- proc(i y)false : (i -> b)
\
\_________
G1 |- x : (i -> b) G1 |- 10 : i \
--------------------------------- \
G1 |- (x 10) : b G1 |- x : (i -> b) \
-----------------------------------------------------------
G1 = {x:(i -> b)} |- if (x 10) then x else proc(i y)false
: (i -> b)
------------------------------------------------------------
{} |- proc((i -> b) x)if (x 10) then x else proc(i y)false
: ((i ->b) -> (i -> b))
Objects
---------------------------------------------------------
+ Interpreter
An object consists of a class tag, plus a value for
each field declared/inherited by the class. In our
language, every field is initialized with 0.
In the case of a flat object representation, the field
values are stored in an vector.
The fields are ordered so that an instance of a class
C --- whether created by "new C" or "new C2" for a class
C2 derived from C --- always has the value of a certain
field in C at a particular location in its vector.
class c extends object
field a % always at vector position 0
method initialize() a = 17
class cx extends c
field b % always at vector position 1
field c % always at vector position 2
method m(w) begin c = b; b = w; c end
class cy extends c
field d % always at vector position 1
method m(w) begin d = a; a = w; d end
let ox = new cx()
oy = new cy()
in begin send ox m(send oy m(2)); 7 end
> Describe the object bound to ox at the point where
"7" is the current expression.
class tag: cx
field vector: 17 17 0
When an object method is called, a new environment
is constructed for evaluating the method body.
The environment contains "self", "%super", and
a binding for each field in the object, plus
a binding for each method argument.
The fields are ordered in the environment so that
derived class fields can hide superclass fields
using the same name.
> Describe the current environment at the point
where "d" (in the "m" method of "cy") is the
current expression.
env:
w= 2
self= object: class tag: cy
fields: ------------.
%super= 'c |
extends |
d= 17 <--- array for the object ---'
a= 2
extends
empty-env
For normal method calls, the interpreter consults
the tag of the receiver object, then looks for
the method starting with that class in the inheritance
tree.
For super method calls, the interpreter ignores the
tag on the object and uses the tag bound to '%super.
> What is the value of the following program?
class c1 extends object
method initialize()
method m() 1
class c2 extends c1
method m() 10
method n1() send self m()
method n2() super m()
class c3 extends c2
method m() 100
let o = new c3()
in +(send o n1(), send o n2())
% The result is 101, because n1 uses the m
% in c3, while n2 uses the m in c1.
+ Types
To type-check a program using objects, class names
are used as types. A class as a type corresponds
to an instance of the class (or one of its subclasses).
To find the type of a program's expression, first
build a picture of the class hierarchy, then
check the program expression using that information.
> What is the type (if any) of the following program?
class c extends object
method int initialize(int x) x
new c(5)
% The type is "c"
> What is the type (if any) of the following program?
class c extends object
method int initialize(int x) x
new c(new c(10))
% No type, because the initialization
% method, called by "new", wants an "int"
% instead of a "c".
**