Midterm

Date: Thursday, February 25th, 3:00pm

 
Name:

Administrative

This exam is intended for two hours. The question scores sum up to a total of 125 points, but it will be graded “out of a 100” so you can skip more than a whole question, small pieces, or just shoot for a bonus.

Question Grade  
(1) Simple Scheme Programming /10
(2) Higher-Order Functions /14
(3) BNF Grammar /12
(4) Fill in the blanks /15
(5) Structural Recursion with ASTs /20
(6) Extending the FLANG language /18
(7) Language Features /20
(8) Schlac /16
Total /125
 


 

Question 1: Simple Scheme Programming

 10 pts 

Define a precedes function that consumes two values and a list, and determines whether the first value appears before the second value in the list. The result should be true if and only if this is the case, and false otherwise. Here are a few test cases to demonstrate how this function works:

(test (precedes 1 2 '(1 2))) (test (not (precedes 1 2 '(2 1)))) (test (precedes 2 1 '(2 1))) (test (not (precedes 1 2 '()))) (test (not (precedes 1 2 '(3 4 5 6)))) (test (precedes 'b 'e '(a b c d e f)))
Remember to write a purpose statement and a correct type declaration.

Hints:



 

Question 2: Higher-Order Functions

 14 pts 

Write a commutative function that accepts a binary function and returns one that is guaranteed to be commutative. Obviously, it is undecidable whether a given function is commutative or not for all possible inputs — but we can take the approach of providing a guarantee for all actual uses of the function by calling the input function twice and throwing an error if the two results are not equal. Here are two tests that demonstrate how this is used:

(test ((commutative +) 1 2) => 3) (test ((commutative -) 1 2) =error> "non-commutative function!")
The function should be usable with any types (but the two input types must be the same), and of course, if you don’t happen to hit a specific non-commutative case, then there is no error:
(test ((commutative string-append) "x" "x") => "xx") (test ((commutative string-append) "x" "y") =error> "non-commutative function!")
Again, remember to write a proper type declaration and a brief purpose statement.

Note: the resulting function will have to be twice slower than the input function, but don’t make it even slower than that!



 

Question 3: BNF Grammar

 12 pts 

There’s a known puzzle about a man trying to cross a river with a sheep, a wolf, and a head of lettuce — using a boat that can only hold him and one more item. As the puzzle goes, he must not leave the wolf alone with the sheep or the sheep alone with the lettuce. In this question you will write a grammar that can be used to describe an attempt to solve this puzzle. Here is how a valid solution looks like in this syntax:

begin move(sheep, left, right); move(empty, right, left); move(lettuce, left, right); move(sheep, right, left); move(wolf, left, right); move(empty, right, left); move(sheep, left, right); end
(Note that whitespaces are ignored as usual.)

There are different ways in which this grammar can be written. One extreme would be a grammar that has exactly the above, forcing all well-formed programs to be exactly this solution — but that doesn’t leave any place for mistakes. A saner option would be something like:

<PUZZLE> ::= ...fill-in... <MOVE> ::= move(<OBJECT>, <SIDE>, <SIDE>);
but this means that the grammar allows obvious mistakes like the following move:
move(sheep, left, left);

Write a BNF that is a little more strict: a possible solution is one that is enclosed in begin...end tokens, with an odd number of moves in between, where the first and the last moves go from the left side to the right, and the moves properly alternate directions.

(Note that this still leaves some nonsensical options, like taking the sheep three times from the left to the right. It is possible to make a syntax that accounts for the contents of each side too, and it is even possible to make the grammar specify all correct solutions. However, these alternatives will be more verbose.)



 

Question 4: Fill in the blanks

 15 pts 

For each one of the following expressions, write a definition for blank in Scheme that makes the expression evaluate to 660. For example, a correct answer for this expression:
(* (blank) 2)
is:
(define blank (lambda () 330))
If you think that it is impossible to write such a definition, explain why this is the case. (Remember: this is Scheme, not Schlac or FLANG, or any other language that we’ve seen.)
You do not need to write types for these definitions (unless one is explicitly requested).

Do this for the following expressions.
  1. (* blank)
  2. (* (blank) (*))
  3. (second (map blank (list blank (blank blank))))
  4. (and (blank) blank)
  5. ((lambda (x y) (x y y)) (lambda (x y) (blank x y)) (lambda (x y) (x x y)))


 

Question 5: Structural Recursion with ASTs

 20 pts 

Write a normalize function that takes in a FLANG and a list of symbols and returns a new FLANG that uses these identifiers in sequence from left to right for all of the bound input names. Use the substitution-based FLANG evaluator for this. For simplicity, you can assume that the list of symbols has enough elements, and that the input expression is closed (has no free identifiers).

For example (using parse for the expected output):

(test (normalize (parse "{with {x1 1} {with {x2 2} {+ x2 x1}}}") '(foo bar baz)) => (parse "{with {foo 1} {with {bar 2} {+ bar foo}}}"))
We can define a convenient helper function for tests, using a fixed list of symbols:
(: norm : String -> FLANG) (define (norm str) (normalize (parse str) '(a b c d e f))) (test (norm "{+ 1 2}") => (parse "{+ 1 2}")) (test (norm "{with {x 1} {+ 1 2}}") => (parse "{with {a 1} {+ 1 2}}")) (test (norm "{with {x 1} {+ {fun {x} x} {with {x 2} x}}}") => (parse "{with {a 1} {+ {fun {b} b} {with {c 2} c}}}")) (test (norm "{with {x {with {x 1} x}} {+ x {fun {x} x}}}") => (parse "{with {a {with {b 1} b}} {+ a {fun {c} c}}}"))

Note that you’ll need to define a helper function that returns two values: the normalized FLANG expression, and the list of leftover symbols. Use this declaration:

(: normalize* : FLANG (Listof Symbol) -> (List FLANG (Listof Symbol)))
where (List A B) is the type of two-item lists with an A value and a B value. (In this case, A is FLANG and B is (Listof Symbol).)

Hint: since you’re using FLANG, you have subst — using it will make the solution very easy.



 

Question 6: Extending the FLANG language

 18 pts 

A relatively recent dialect of Lisp is “newLISP®”; its most obvious feature is that it is a dynamically-scoped “interpreted” language — and this is considered a feature (though you won’t find it listed on the front page). Regardless of whether this is a good idea or not, we can play with a feature that is central to it: first-class environments.

The idea of the extension that we’re going to implement is that at any point you can grab the current environment (as a new kind of value), and later on you can use it for some computation. Note that we will be extending the environment-based FLANG-ENV code which is lexically scoped, but the extension itself will make the language have dynamic-scope properties.

Our extension will involve two new expressions:

<FLANG> ::= ...same... {get-scope} {with-scope E1 E2}
get-scope will evaluate to an environment value that holds the current environment, and with-scope evaluates E1 to get such a value, and proceeds to evaluate E2 in that environment. get-scope can therefore be considered as something similar to a closure, except that it needs to be used explicitly.

Here’s an example that should demonstrate this:

(test (run "{with {e {with {x 1} {with {y 2} {get-scope}}}} {with-scope e {+ x y}}}") => 3)
and it can be used to create fake closures, as in the following example that mimics a curried addition function:
(test (run "{with {add {fun {x} {with {r {fun {y} {+ x y}}} {get-scope}}}} {with-scope {call add 1} {call r 2}}}") => 3)
(Note that we’re still in a lexically scoped language, but this example doesn’t use a closure for the result of add.)

To implement this, we begin with an extension of the AST:

(define-type FLANG ...same... [GetScope] [WithScope (scope-expr FLANG) (body FLANG)])
and the parser is extended accordingly. We then extend the kind of values:
(define-type VAL ...same... [ScopeV (env ENV)])

The last thing to do is to implement the evaluation fragments for the two new expressions. Do that.



 

Question 7: Language Features

 20 pts 

Each of these questions has multiple choices. Choose the correct answer. Unless stated explicitly, there is only one correct answer.
  1. Our commutative function (from the first question) relies on a certain property of the input function to preserve its meaning. Which of the following functions does preserve its semantics when fed into commutative? In other words, for which of these functions can you say that (commutative F) is the same as F (modulo its runtime):
    1. (lambda (x y) (printf "adding ~s+~s\n" x y) (+ x y))
    2. (lambda (x y) (* (+ x y) (read-number)))
      (where read-number reads a number from the user)
    3. (lambda (x y) (* (+ x y) ((lambda (x) (x x)) (lambda (x) (x x)))))
    4. (lambda (x y) (* (+ x y) (error "poof!")))
  2. Why do we need to assume that the input to normalize (from question number 5) is closed (has no free occurrences)?
    1. Because code that is not closed cannot be evaluated, therefore normalizing it is not necessary.
    2. Because doing that means that normalization does not preserve the semantics of the original code — we will get a dynamically scoped language.
    3. Because doing such normalization can lead to capturing identifiers, for example, if we rename a binding instance to x and its scope had a free x.
    4. Because doing such normalization can lead to capturing identifiers, which could be avoided at the cost of a more complex implementation which will avoid using them.
    5. Because our language is strict, otherwise there is no problem with free identifiers as long as we don’t need them for an expression.
    6. Because bound and free occurrences of identifiers have different semantics in a strict language.
  3. Explain why the get-scope and with-scope forms defeat any compilation attempts. (In the sense of using de-Bruijn indexes.)
    1. As can be seen in the examples, we can write code with references to identifiers that are not in the current lexical scope, therefore we get dynamic scope and we cannot compile code.
    2. Since we mimic a feature of a dynamically scoped language, attempts to compile the code can get the compiler into an infinite loop.
    3. It is impossible to compile the code only because we implement a dynamic-scope feature on top of a lexically scoped language. If we had only dynamic scopes to begin with, we could use the same compilation strategy that was discussed in class.
    4. The claim is wrong: compilation is still as possible as it is in FLANG without our extension.
  4. Write an expression — if possible — that we can use to learn whether a Schlac-like implementation is dynamically scoped or statically scoped. Explain if impossible.
    1. ((lambda (x) (x x)) (lambda (x) (x x)))
      if the language is dynamically scoped, this will throw an “unbound identifier x” error
    2. (define x 4)
      (define (return-x) x)
      (let ([x 5]) (return-x))

      if the language is dynamically scoped, we’d get 5, otherwise the result is 4.
    3. It is impossible to tell whether it uses dynamic scope or lexical scope, because define in the language cannot be used for recursive functions anyway.
    4. It is impossible to tell whether it uses dynamic scope or lexical scope, because the language is lazy.
    5. It is impossible to tell whether it uses dynamic scope or lexical scope, because the language is implicitly curried.
    6. ((lambda (x y) x) 5 6)
      if the language is dynamically scoped, this will throw an “unbound identifier x” error


 

Question 8: Schlac

 16 pts 

In class we have seen two ways to define not for our lambda encoding of booleans:

(define not1 (lambda (a) (a #f #t))) (define not2 (lambda (a) (lambda (x y) (a y x))))
One thing that was mentioned is that the two definitions are equivalent only when viewed as a unary boolean function, so they’re different combinators that happen to have the same result when used with encoded booleans.

We will show this now formally as follows: take both definition as a function from two-argument-numeric-functions (Number Number -> Number) to two-argument-numeric-functions functions and see that they produce different output functions for the same input. We only need one counter-example, so we’ll use + as our two-argument-numeric-function and show that (not1 +) and (not2 +) return two different two-argument-numeric-function.

First, begin with (not2 +) — to see what it means, we’ll apply it to two arbitrary encoded church numerals, and reduce things from there:

((not2 +) A B) (((lambda (a) (lambda (x y) (a y x))) +) A B) ; definition of not2 ((lambda (x y) (+ y x)) A B) ; application step (+ B A) ; another application

The second step is to do the same with (not1 +):

((not1 +) A B) (((lambda (a) (a #f #t)) +) A B) ((+ #f #t) A B) ...
Continue the above derivation.



 

Feedback Question

[This question is optional, of course. Feel free to write more details, give a single word reply for each item, or completely ignore it if you’re not interested.]

Exam:
Class:
Homeworks:
Any other general comments?