Assignment 6: Programming with Continuations
Instructions:
- Due date: Fri. Nov 15, 11:59PM EST
- Upload your solutions to gradescope as
problem-1.rkt
,problem-2.rkt
, andproblem-3.rkt
. - Ask questions on Piazza or during office hours.
- Perform all the work for this project by yourself. You may discuss with your classmates, but you must write your own code and solve your own exercises.
- Starter code:
Problem 1: Tail Form [20 Points]
In class, you saw how you can convert a recursive function into a tail-recursive one. Convert the following Racket functions into their tail-recursive form. You can do either the continuation approach or the custom accumulator one. Do not change the signature of the function (you can write helper functions). Don’t forget that only recursive functions needs to be converted into tail form.
;;; reverse-list : List A -> List A ;;; Returns a new list of the same length with the order of elements reversed. (define/contract (reverse-list xs) (parametric->/c [A] (-> (listof A) (listof A))) (match xs ['() '()] [(cons x rest) (append (reverse-list rest) (list x))]))
;;; fold-right : (A -> B -> B) -> B -> List A -> B ;;; Combines the elements of the list into a single result of type B. ;;; Evaluated right to left (f x[0] (f x[1] ... (f x[n] init))) (define/contract (fold-right f init xs) (parametric->/c [A B] (-> (-> A B B) B (listof A) B)) (match xs ['() init] [(cons x rest) (f x (fold-right f init rest))]))
3.
;;; type tree =
;;; | Leaf
;;; | Node of tree * Int * tree
(define-struct leaf ()
#:transparent)
(define-struct node (left val right)
#:transparent)
;;; tree-sum : tree -> Int
;;; Sums up all the values in the tree
(define/contract (tree-sum tree)
(-> (or/c leaf? node?) integer?)
(match tree
[(leaf) 0]
[(node left val right) (+ (tree-sum left) val (tree-sum right))]))
Problem 2: Named Exceptions [40 Points]
Many programming languages, like Racket, allow programmers to catch and handle different variants of exceptions separately:
(struct exn:fail:odd-number exn:fail ())
(define (raise-odd-number-error)
(raise (exn:fail:odd-number "odd number" (current-continuation-marks))))
(define (raise-divide-by-zero-error)
(raise (exn:fail:contract:divide-by-zero "divide by zero" (current-continuation-marks))))
(define (is-even n)
(with-handlers ([exn:fail:user? (λ _ #t)])
(with-handlers ([exn:fail:odd-number? (λ _ #f)])
(with-handlers ([exn:fail:contract:divide-by-zero? (λ _ #t)])
(cond
[(= n 0) (raise (raise-user-error "zero is even!"))]
[(= (modulo n 2) 0) (raise-divide-by-zero-error)]
[else (raise-odd-number-error)])))))
Notice, because exn:fail:odd-number
is not handled by the third (inner-most) with-handlers
block, it bubbles up and is caught by the second with-handlers
block. Likewise, any raised exn:fail:user
exxception bubbles up past the second and third with-handlers
blocks before being caught by the first (outer-most) with-handlers
block. Hence, this function works as intended:
> (is-even 2)
#t
> (is-even 7)
#f
In this problem, we will extend the try-catch language from lecture to include an arbitrary number of exception handlers, following the semantics of the Racket code presented above.
Consider the following $\lambda$ calculus with try-catch:
(struct enum (n)
#:guard (struct-guard/c integer?))
(struct eadd (e1 e2)
#:guard (struct-guard/c (recursive-contract expr/c) (recursive-contract expr/c)))
(struct eident (id)
#:guard (struct-guard/c string?))
(struct elam (param body)
#:guard (struct-guard/c string? (recursive-contract expr/c)))
(struct eapp (e1 e2)
#:guard (struct-guard/c (recursive-contract expr/c) (recursive-contract expr/c)))
(struct etry (body code handler)
#:guard (struct-guard/c (recursive-contract expr/c) integer? (recursive-contract expr/c)))
(struct eraise (code)
#:guard (struct-guard/c integer?))
(define expr/c (or/c enum? eadd? eident? elam? eapp? etry? eraise?))
(define value/c (or/c enum? elam?))
Your primary task is to implement the interpreter interp
, a continuation-passing style interpreter that interprets expressions from the exception language, using interp/helper
and subst
as helper functions. The interpreter should satisfy the following criteria:
enum
,eadd
,eident
,elam
, andeapp
correspond to numbers, addition, variables, $\lambda$ terms, and function application in the $\lambda$-calculus, respectively.eraise
follows the syntax(eraise exception-code)
, whereexception-code
is an integer representing the variant of exception that is raised.etry
follows the syntax(etry try-expr exception-code catch-expr)
.etry
will attempt to interprettry-expr
and catch any raised exceptions with codeexception-code
by interpretingcatch-expr
. If an exception with a different code thanexception-code
is raised, then it “bubbles up” (as illustrated in the example above).- If the interpreter encounters an uncaught exception, it raises an “uncaught exception” exception by using
(raise-uncaught-exn-error)
. - If the interpreter encounters an unbound identifier, it raises a runtime exception by using
(raise-runtime-error)
.
Note: In order to receive full credit for your solution, your interpreter must utilize continuations and be in tail form.
Problem 3: Non-determinism [40 Points]
Many problems in computer science involve backtracking searching through a space of possible solutions. In this problem, we will make a small programming language for solving problems that involve backtracking search by introducing non-determinism and failure.
Here is the abstract syntax of the Amb
language, introduced by John McCarthy in 1963:
(struct eif (guard thn els)
#:transparent
#:guard (struct-guard/c (recursive-contract expr/c) (recursive-contract expr/c) (recursive-contract expr/c)))
(struct eand (e1 e2)
#:transparent
#:guard (struct-guard/c (recursive-contract expr/c) (recursive-contract expr/c)))
(struct enot (e1) #:transparent
#:guard (struct-guard/c (recursive-contract expr/c)))
(struct ebool (v) #:transparent
#:guard (struct-guard/c boolean?))
(struct eident (id)
#:transparent
#:guard (struct-guard/c string?))
(struct echoose (id body)
#:transparent
#:guard (struct-guard/c string? (recursive-contract expr/c)))
(struct efail () #:transparent)
(define expr/c (or/c eif? echoose? eand? enot? efail? ebool? eident?))
The language has some familiar constructs (conditionals, and, not, Booleans, and identifiers), and two new constructs: echoose
and efail
. Their semantics are:
(echoose id body)
is similar tolet
: it non-deterministically picks a Boolean value and assigns it toid
. Then, it runsbody
.(efail)
denotes failure: it means that this run of the program is invalid and should be rejected, and another assignment to the non-deterministic choices should be tried instead.
An Amb
program is valid if there is an assignment to non-deterministic choices that does not evaluate (efail)
, and invalid if all assignments to non-deterministic choices evaluate (efail)
. For example, the following Amb
programs are valid:
(ebool #t)
(echoose "x" (eident "x")) ; valid assignments: x = t or x = f
(echoose "x" (eif (eident "x") (efail) (ebool #t))) ; valid assignment: x = f
The following Amb
programs are invalid:
(efail)
(echoose "x" (eif (eident "x") (efail) (efail)))
Your task is to implement the interp
function that returns #t
if an Amb
program is valid and #f
if it is invalid. Your solution must involve continuations to receive full credit.