Sample Final 4

Date: Monday, April 26th

 
Name:

Administrative

This exam was given in the Fall semester of 2009.

Question Grade  
(1) Scheme Programming /20
(2) Language Extension: Extending Scheme /18
(3) Language Extension: Extending Toy /32
(4) Lazy Languages /18
(5) Language Features /20
(6) Type System /20
Total /128
 

Remember: the correct answers are short. Also, questions that ask for a verbal answer rather than code should still be answered correctly and based on facts.



 

Question 1: Scheme Programming

 20 pts 

As we have discussed in class, we can get the benefits of functional (side-effect-free) programming in an eager side-effect-full language by using thunks (nullary functions) to wrap side-effects, turning them into plain values. Using this, you get a simple functional world with no side-effects to complicate things, while still being able to conveniently interact with the “real world”.

For example, say that we have a set of Logo-like drawing primitives, including:

(: pen-down : -> Void) ;; lowers the pen down to the paper (: pen-up : -> Void) ;; raises the pen from the paper (: move : Number -> Void) ;; moves the pen N pixels in its current direction, drawing if ;; it is currently on the paper (: turn : Number -> Void) ;; turn the direction the pen will move by N degrees
Note that all of these return Void — they’re used only for their side-effect, not for their value. To represent these functions as values rather than operations, we wrap them all inside thunks. We use CamelCase to indicate that these values are constructors:
(: PenDown : -> (-> Void)) (define (PenDown) pen-down) (: PenUp : -> (-> Void)) (define (PenUp) pen-up) (: Move : Number -> (-> Void)) (define (Move n) (lambda () (move n))) (: Turn : Number -> (-> Void)) (define (Turn n) (lambda () (turn n)))
[Note that pen-down and pen-up are already thunks, but they’re still wrapped in a second thunk layer — this is done for consistency, so they’re still used as constructors rather than values.]

Now, to use these values, we need some convenient utilities. Some of these utilities can be specific to the Logo world, for example:

(: WithPenDown : (-> Void) -> (-> Void)) ;; turns an operation into one that is performed with the pen ;; temporarily down (assuming that the pen usually stays up). (define (WithPenDown op) (lambda () (pen-down) (op) (pen-up)))
However, this breaks the functional view that we want to have: we need to write code that does something. What we’re missing here is a way to combine some operations without doing so. For example, if we had a Combine function that combines two operations, we could have written:
(define (WithPenDown op) (Combine (Combine (PenDown) op) (PenUp)))
Note that this uses PenDown and PenUp — but there is no explicit code here that is part of the actual execution — only code that describes operations in terms of other operations.

  1. Implement this Combine constructor. (Note that the constructor itself needs to go down to the “doing something” level, since that is exactly what it abstracts away.) Remember to write a correct type and a purpose statement.
  2. Add another operation called Repeat that consumes an integer N and a thunk T, and returns a new thunk that will apply T N times when invoked. For example, using this we could define a square as follows:
    (: Square : Number -> (-> Void)) ;; make a square of the given size (define (Square n) (Repeat 4 (Combine (WithPenDown (Move n)) (Turn 90))))
    Note that this could be implemented in one of two ways: Again, remember to write a correct type and a purpose statement.


 

Question 2: Language Extension: Extending Scheme

 18 pts 

In several occasions, we have seen how curried functions can be a very useful tool. To make it convenient to write curried functions, we want to extend Scheme with a new form: clambda, that creates a curried lambad. To demonstrate how it is used:

(clambda (a b c d) (+ (* a b) (* c d))) ; should expand to (lambda (a) (lambda (b) (lambda (c) (lambda (d) (+ (* a b) (* c d))))))

Implement this clambda form as a macro, using define-syntax and syntax-rules. You don’t need to make it work on no arguments — there is no point in currying that case. Note that the solution is relatively short (about 4-6 lines should be sufficient).



 

Question 3: Language Extension: Extending Toy

 32 pts 

In Assignment #10 we have implemented an extension to the TOY language that has by-reference rfun functions. An alternative approach is what you get in C: extend the language with a new kind of “reference” values, which hold the box that is the location of some value. This extension should be a familiar process: we’re taking some feature of our language that is currently second-class, and we promote it to a first class value that is accessible from inside the language. In the C case, this means exposing raw machine pointers; and in our case, we take the boxes that are used to store binding values and expose them. These boxes are currently available only in the extended TOY implementation — in Scheme code, and we will make them accessible inside the language, so they can be passed around and used as any other kind of values in TOY programs.

To get a quick idea of how this extension will look like, here is a test that will eventually work:

(test (run "{bind {{x 1} {incref! {fun {x} {setref! x {+ x 1}}}}} {bind {{*x {ref x}}} {incref! *x} {incref! *x} *x}}") => 3)
(Note that *x is just a plain identifier here.)

As usual, we begin by extending the BNF,

<TOY> ::= <num> | <id> | { ref <id> } ; <-- new special form | { set! <id> <TOY> } | ...same...
the AST type definition,
(define-type TOY [Num (n Number)] [Id (name Symbol)] [Ref (name Symbol)] ; <-- new variant [Set (name Symbol) (new TOY)] ...same...)
and the parser is extended accordingly. We then extend the kind of TOY values with a new “reference” type:
(define-type VAL [BogusV] [ScmV (x Any)] [RefV (b (Boxof VAL))] ; <-- new value type [FunV (names (Listof Symbol)) (body (Listof TOY)) (env ENV) (byref? Boolean)] [PrimV (prim ((Listof VAL) -> VAL))])
And we now proceed to implement the new functionality: evaluating the new special form and a function that uses the new kind of values.

  1. To evaluate the new special form, we need to have an additional case in the body of the eval function for a Ref syntax, which should return a new RefV value holding the appropriate box:
    [(Ref name) (RefV —«fill-in»—)]
    Fill in the missing code here.
  2. Now that we have a working way to construct these values, we need to implement a way to use them. We do this with a new setref! primitive function, that can be used as:
    {setref! foo bar}
    to set the value referenced by foo to the bar value. However, we need to implement it directly rather than construct it with scheme-func->prim-val. First, the binding is added with:
    (define global-environment (FrameEnv (list (list '+ (scheme-func->prim-val +)) ...same... (list 'setref! (box (PrimV setref-function))) ;; values ...same...) (EmptyEnv)))
    and now we need to implement it as setref-function. We need to follow the same calling protocol as for all primitive functions, meaning that it should be a function from a list of VAL inputs to a VAL output, and and this means that we need to check that we’re getting the right number of input arguments. Finally, we also need to make sure that the value that we receive is indeed a RefV:
    (: setref-function : (Listof VAL) -> VAL) ;; the implementation of the builtin "set-ref!" function that changes ;; the contents of a ref value (define (setref-function args) (if (= (length args) 2) (cases (car args) [(RefV b) —«fill-in»—] [else (error 'setref! "got a non-ref value: ~s" (car args))]) (error 'setref! "bad number of arguments, expecting two")))
    Fill in the missing code.
    (Hint: there are two expressions that are needed there. (But both are very short.))
  3. Now comes an important part: users will often want features that make their life easier, even when in the long run the potential for problems is higher. In this case, your users want to be able to treat these reference values (RefV for you) as if they were not there. For example, in the sample test code given above note that the mutation expression is:
    {setref! x {+ x 1}}
    so x is used as a plain value too. To give our users the maximum comfort (and maximize their chance of shooting their feet), we’re going to implement this. First, we write a utility function that consumes some VAL, and if it happens to be a RefV, it will dereference it, otherwise it will return the value it received as-is:
    (: deref-value : VAL -> VAL) ;; utility function that automatically converts a ref value to a plain ;; value (define (deref-value val) —«fill-in»—)
    Fill in the missing part. Note that this is again a very simple function (to be more specific, you don’t need any loops or recursion or evaluation).
  4. Finally, we need to use this deref-value in places where a value is needed. The first such place is in the primitive function application, in the body of scheme-func->prim-val. This code:
    (map (lambda: ([a : VAL]) (cases a [(ScmV v) v] [else (error ...)])) args)
    needs the value held in a — so we change it to:
    (map (lambda: ([a : VAL]) (cases (deref-value a) [(ScmV v) v] [else (error ...)])) args)
    There are three more places that need to use deref-value, find them. (You only need to specify which expressions need it, there’s no need to actually change the code.)


 

Question 4: Lazy Languages

 18 pts 

Question 3 was somewhat similar to the Sloth implementation in that a certain value is automatically converted to another one. But there is one thing that we did in the Sloth implementation, but didn’t do here: in Sloth, note that there is a second boolean argument to scheme-func->prim-val, which determines if values are forced or not, and was used in a particular way by the global list binding (of the Sloth language).

You need to figure out what is it exactly that is missing, and how it would affect the code if it is added as in the Sloth implementation. That is, understand what the extra boolean flag causes if our evaluator is also extended with it. You will need to assume that the language is also extended with list or cons.

Once you know what will change, demonstrate this: you need to write a sample program for our extended TOY evaluator, such that it will return a different result if the boolean flag is added (and the code in scheme-func->prim-val is changed to use it as Sloth does) or if not.

(Note: it’s TOY sample code that is needed here.)



 

Question 5: Language Features

 20 pts 

In the following sub-questions, you should choose the valid answer. Only one should be chosen, unless the question explicitly says otherwise.

  1. In Question 3 we have added ref as a new built-in special form, rather than a primitive function — unlike setref!. Is it not possible to implement it as a function instead?
    1. It is possible, but implementing it as a new form makes the implementation faster.
    2. It is possible, but implementing it as a new form makes the implementation simpler.
    3. It is possible to do that: functions have uniform evaluation rules, but we could do what we did with set-ref!, and implement the function directly in a way that can access the box.
    4. It is not possible to do that: functions have uniform evaluation rules, so any function we would write would receive a plain value instead of the box.
    5. It is not possible to do that, because the new function would need to use eval, which leads to an infinite loop that prevents getting any actual box value.
  2. Does it make sense to define a while macro in a lazy language? There are two of valid answers here, you need to find both. (Note: they don’t have to be both Yes or No answers (*wink* *wink*).)
    1. No, just like it doesn’t make sense in plain Scheme, otherwise it would have been part of the language.
    2. No, because an implementation of while in Scheme relies on having proper tail calls, and lazy languages cannot support that.
    3. No, because macros are not needed in a lazy language.
    4. No, because while is only useful with side-effects, and side-effects are problematic in a lazy language.
    5. Yes — in exactly the same way that it makes sense to do that in plain Scheme.
    6. Yes, but since it’s a lazy language, it could be defined as a simple function.
    7. Yes, provided that we have a representation that encodes side effects, but then it could be defined as a simple function.
  3. In Lecture #17 we’ve seen how we can implement a TOY to Scheme compiler (more precisely TOY to our dialect of Typed Scheme). As a TOY programmer, how can you determine whether the language is compiled using this or interpreted as usual?
    1. You write an infinite loop and see if the stack blows up.
    2. Impossible: you could use an infinite loop, but that TOY implementation doesn’t have bindrec, so no loops are possible.
    3. Impossible: the compiler that we designed preserves the semantics of the original language.
    4. You write a TOY expression with a type error in it, and if it’s compiled to Scheme, then the resulting Scheme code would fail to typecheck and throw an error, otherwise it will run fine.
    5. You write code that implements the naive fib function, and measure how fast it runs with a sufficiently large input number. The difference between a compiled language and an interpreted one will be obvious since the first is going to be much faster.
    6. You write code that generates code at runtime, if it’s gets compiled then the resulting lanugage will not have a compiler and therefore will have a runtime error, otherwise it will work fine.
  4. Is there any danger in having recursive types in the typed-scheme-based language that we used in the course?
    1. There is no danger with recursive types.
    2. Typed Scheme doesn’t have recursive types.
    3. Our dialect of typed scheme doesn’t have recursive types.
    4. Even if we didn’t have recursive types explicitly, we could emulate them using the Y combinator, so there is no new danger.
    5. Using a recursive type is dangerous because it can get the type checker stuck in an infinite loop.
    6. Using a recursive type is useful, but if you have typed macros it is easy to get the type checker stuck in an infinite loop.
    7. The type language is lazy, so there is no problems with recursion.


 

Question 6: Type System

 20 pts 

Finally, to continue with the spirit of mutation, we will extend the statically-typed Picky language with mutation. Start with the basic Picky language definition, given here so you don’t need to wonder which version you need:

<PICKY> ::= <num> | <id> | { fun { <id> : <TYPE> } : <TYPE> <PICKY> } | { + <PICKY> <PICKY> } | { call <PICKY> <PICKY> } <TYPE> ::= Number | ( <TYPE> -> <TYPE> ) Γ ⊢ n : Number Γ ⊢ x : Γ(x) Γ ⊢ a : Number Γ ⊢ b : Number Γ ⊢ {+ a b} : Number Γ[x:=τ1] ⊢ E : τ2 Γ ⊢ {fun {x : τ1} : τ2 E} : (τ1 -> τ2) Γ ⊢ f : (τ1 -> τ2) Γ ⊢ v : τ1 Γ ⊢ {call f v} : τ2

You need to add the following to the BNF:

Once you have that, write two new typing judgements — one for set!, which should evaluate to a Void value, and one for begin, which should evaluate to the result of its second (and last) subexpression. Be careful with set! — it should not be possible to change the type of an identifier, since this will break the invariants that the type checker enforces.