Assignment 4: Let and fun
Objectives:
- Become comfortable with the basic scope, local binding, and programming with local functions.
Resources:
- The Plait tutorial and documentation page: https://docs.racket-lang.org/plait/
Parameters:
- Due date: Friday Feb. 9 at 11:59PM
- Upload your solution as a
code.rkt
file on Gradescope. - Ask questions on Piazza or during Office Hours.
- Use the following starter code
- Perform all the work for this project by yourself. You may discuss with your classmates, but you must write your own code.
- Grading read carefully: each problem is worth a certain number of points. The points given for each problem is the fraction of tests passed for that problem (i.e., if 50% of the tests are passed for a 20 point problem, 10 points are given). Update: Your grade will be determined solely by the public autograder score on this assignment.
Problem 1: Scope? Nope!
Consider the following let
language from class:
(define-type Exp
[numE (n : Number)]
[plusE (l : Exp) (r : Exp)]
[varE (s : Symbol)]
[let1E (var : Symbol)
(assignment : Exp)
(body : Exp)])
(define (subst id assignment body)
(type-case Exp body
[(numE n) (numE n)]
[(plusE l r) (plusE (subst id assignment l) (subst id assignment r))]
[(varE s) (if (symbol=? s id) (numE assignment) (varE s))]
[(let1E innervar innerassignment innerbody)
(let1E innervar (subst id assignment innerassignment)
; check for shadowing
; if id is being shadowed, then return inner body
; else, substitute inner body
(if (symbol=? innervar id)
innerbody
(subst id assignment innerbody))
)]))
(define (eval e)
(type-case Exp e
[(numE n) n]
[(plusE l r) (+ (eval l) (eval r))]
[(varE s) (error 'runtime "uninitialized")]
[(let1E id assignment body)
;Semantics of (let1E var assignment body):
;(1) evaluate assignment to value
;(2) substitute `var` with value in body
;(3) evaluate body
(eval (subst id (eval assignment) body))
]))
Include in your submission two components:
Problem 1a. Define two functions: broken-subst
and broken-eval
. broken-eval
is identical to eval
except
broken-subst
is used in place of subst
. broken-subst
, unlike subst
, causes the following tests to pass:
(test (broken-eval (let1E 'x (numE 10)
(let1E 'x (numE 20) (varE 'x)))) 10)
(test (broken-eval (let1E 'x (numE 20)
(let1E 'x (plusE (varE 'x) (numE 5)) (varE 'x)))) 20)
Problem 1b. Using your eval
and broken-eval
, define two Exp
s:
- An
Exp
namedunaffected
thateval
uates to the same value, when evaluated using botheval
andbroken-eval
, and - An
Exp
namedaffected
thateval
uates to different values.
Use test
to make sure that unaffected
and affected
exhibit the correct behavior. You are not allowed to use any of the test cases defined above.
Problem 2: Uniquify
You are tired of dealing with shadowing, so you’ve decided to implement a function called uniquify
that, when given an abstract syntax tree for the let
language, renames all shadowed variables to unique names. This way, you can tell a variable’s scope just by looking for its unique binding in the program!
You run into a challenge almost immediately: even if we generate unique names, we need a way to replace the existing name in your abstract syntax tree with your newly generated unique one. Notice how our current subst
function does not do that yet.
Problem 2a. Write function update-names
such that, (update-names e id id2)
replaces all instances variables with name id
in e
with the name id2
. You can assume that id2
will never appear in e
.
Example behavior:
(test (update-names
(let1E 'x (numE 10)(varE 'x)) 'x 'y)
(let1E 'y (numE 10) (varE 'y)))
(test (update-names
(let1E 'x (varE 'x) (let1E 'x (numE 10) (varE 'x))) 'x 'y)
(let1E 'y (varE 'y) (let1E 'y (numE 10) (varE 'y))))
Problem 2b. Implement uniquify
using update-names
. Your solution should additionally use the following fresh-name!
function, which you can assume is guaranteed to produce a unique name whenever it is called (don’t worry about understanding how it works; we will cover mutation later):
(define counter (box "###"))
(define (fresh-name!)
(string->symbol (begin
(set-box! counter (string-appnd "a" (unbox counter)))
(unbox counter))))
Try calling fresh-name!
a few times and see how it behaves.
If we assume variables can’t be shadowed, we can simplify our let
evaluator to remove the check for shadowing inside subst
:
(define (subst-uniq id assignment body)
(type-case Exp body
[(numE n) (numE n)]
[(plusE l r) (plusE (subst-uniq id assignment l) (subst-uniq id assignment r))]
[(varE s) (if (symbol=? s id) (numE assignment) (varE s))]
[(let1E innervar innerassignment innerbody)
(let1E innervar (subst-uniq id assignment innerassignment)
(subst-uniq id assignment innerbody))
]))
(define (eval-uniq e)
(type-case Exp e
[(numE n) n]
[(plusE l r) (+ (eval-uniq l) (eval-uniq r))]
[(varE s) (error 'runtime "uninitialized")]
[(let1E id assignment body)
(eval-uniq (subst-uniq id (eval-uniq assignment) body))
]))
Example behavior:
(test
(eval-uniq (uniquify
(let1E 'x (numE 10)
(let1E 'x (numE 20)
(varE 'x)))
))
20)
Hint: Your solution will likely require a helper function that maintains an environment, like we’ve seen in lecture. You can make use of any features of Plait in your solution. You may find the list member
function useful (see the Plait documentation here)
Problem 3: Lambda the ultimate boolean
Recall from lecture our untyped λ-calculus:
(define-type LExp
[var (s : Symbol)]
[lam (x : Symbol) (body : LExp)]
[app (e : LExp) (arg : LExp)])
(define-type Value
[fun (arg : Symbol) (body : LExp)])
; retrieve argument name from a function
(define (get-arg v)
(type-case Value v
[(fun arg body) arg]))
; retrieve function body from a function
(define (get-body v)
(type-case Value v
[(fun arg body) body]))
; perform e1[x |-> e2]
(lam-subst : (LExp Symbol LExp -> LExp))
(define (lam-subst e1 x e2)
(type-case LExp e1
[(var s) (if (symbol=? s x) e2 (var s))]
[(lam id body) (if (symbol=? x id) (lam id body) (lam id (lam-subst body x e2)))]
[(app e1App e2App) (app (lam-subst e1App x e2) (lam-subst e2App x e2))]))
; runs a λ-calculus expression
(define (interp e)
(type-case LExp e
[(var s) (error 'runtime "unbound symbol")]
[(lam id body) (fun id body)]
[(app e1 e2)
; run e1 to get (lambda (id) body)
; run e2 to get a value argV
; run body[id |-> v]
(letrec [(e1V (interp e1))
(body (get-body e1V))
(id (get-arg e1V))
(argV (interp e2))]
(interp (lam-subst body id (lam (get-arg argV) (get-body argV)))))]))
And consider the below subset of the IteExp
language we discussed as well:
(define-type BExp
[boolE (b : Boolean)]
[iteE (guard : BExp) (thn : BExp) (els : BExp)])
In this problem we will Church encode expressions of type BExp
. Church encodings are representations of data types (like Booleans) and their manipulation (like if-then-else) into pure functions in the λ-calculus. Such encodings have been used for a long time as both mathematical objects of study and also foundations for compilers for real languages, namely Lisp.
Problem 3. Define a function church-encode
of type (BExp -> LExp)
that returns the Church encoding of an arbitrary BExp
. To help you out, recall the Church encodings of #t
and #f
we reviewed in class:
(church-encode (boolE #t)) ---> (lam 'x (lam 'y (var 'x)))
(church-encode (boolE #f)) ---> (lam 'x (lam 'y (var 'y)))
One way to tackle the last case is to consider how Booleans work functionally in our language. In BExp
, Booleans are used by if-then-else expressions:
(iteE (boolE #t) (boolE #t) (boolE #f)) should evaluate to (boolE #t),
(iteE (boolE #f) (boolE #t) (boolE #f)) should evaluate to (boolE #f).
In the λ-calculus, there is no notion of if-then-else, so we must resort to using our Booleans as functions.