home—lectures—recipe—exams—hws—D2L—breeze (snow day)
hw08
O6: Adding environments
static scope; closures
Due Dec.06 (Fri) at the start of class;
accepted through Dec.12 (Thu) 17:00.
Submit:
a hardcopy with
the additional tests for O6 (since O4),
and the additional/changed function(s) for O6 (since O4).
Put under my office door, if I'm not there.
Submit all files on D2L.
Since O6 is an extension O5, you don't need any code or test-cases for O5 different from O6.
We continue to build on
the O language implementation from previous homeworks
(O2 specs, solution (.java,.rkt)
O4 specs, solution available 2013.Dec.03 0:01
(.java,.rkt).)
You may implement this homework in either Java or Racket (or another language, if you clear it with me).
Please indicate in your submitted file, what sections of code are udpated
and what is unchanged from earlier homeworks/solutions.
You don't need to turn in any hardcopy of unchanged-code
(but submit a fully-working copy in the drop-box).
- (10pts) Prolog recursion
-
Write
allLike(Ppl,Drnk)
which is true iff
every person in the list Ppl drinks Drnk.
(Note that Ppl does not need to contain only unique people.)
-
How would you use your relation to find
a list-of-three-people who drink pepsi,
where the second person in the list is bob?
-
How would you query all lists
of people who drink pepsi?
(That is, each solution found by Prolog should be a list of people,
where everybody in the list likes pepsi.)
How many such lists are there, in your knowledge base?
When you type this query in,
what are the first three results prolog gives you?
-
Write
allLike(Ppl) which is true iff
every person in the list Ppl
have some drink preference in common.
(That is, if there is some drink that everybody in the list likes.)
- (10pts) Prolog list
Write the following Prolog predicates.
Do not use append.
- (3pts)
last(List, Item),
which succeeds exactly when
Item is the last item in List.
This rule should fail if List is empty, of course.
(This happens to be the book's Chpt.16, programming exercise #6.)
- (2pts)
nextToLast(List, Item)
which succeeds exactly when
Item is the next-to-last item in List.
(This rule should fail if List has fewer than two items, of course.)
- (2pts)
lastTwoReversed(List, ListOf2)
which succeeds exactly when
ListOf2 contains the last and the second-to-last item
of List
(in that order).
- (3pts)
reverseLastTwo(List, NewList)
succeeds exactly when NewList
is like List
except that the last two items have been reversed.
(This rule will fail if List has fewer than two items.)
All of the predicates fail if the first argument is not a list.
Some examples (test cases) are provided, below.
Note that xsb Prolog contains several list functions which you are NOT to use for this assignment
(e.g. append and reverse).
Also, for full credit, don't merely reverse the input list and operate on that result.
As ever,
your program should follow good style,
including
appropriate white space,
meaningful variable names,
and
as well as a header comment with your name, the name of the assignment, etc..
(You can have comments describing how a predicate works if you want;
however, you can also assume your program is being read by somebody
who understands the fundamentals of Prolog.)
-
Optional challenge-problem1
(Scope, 15pts)
The racket form
set! changes the
binding of a variable:
(define x 5)
(set! x (* 2 x))
; the variable x is now bound to 10. |
1. (define a 10)
2. (define b 20)
3. (define make-foo
4. (let {[b 30]}
5. (lambda () ; ← make-foo is bound to this function.
6. (let {[c 40]}
7. (lambda (cmd) ; ← make-foo returns this function as its answer.
8. (let {[d 50]}
9. (cond [(symbol=? cmd 'incA) (set! a (+ a 1))]
10. [(symbol=? cmd 'incB) (set! b (+ b 1))]
11. [(symbol=? cmd 'incC) (set! c (+ c 1))]
12. [(symbol=? cmd 'incD) (set! d (+ d 1))]
13. [(symbol=? cmd 'get-all) (list a b c d)])))))))
|
-
The scope of the a declared in line 1 is lines through .
-
The scope of the b declared in line 2 is lines through .
-
The scope of the b declared in line 4 is lines through .
-
The scope of the c declared in line 6 is lines through .
-
The scope of the d declared in line 8 is lines through .
Suppose that make-foo is called exactly three times
(but that a function returned by make-foo is not called).
-
How many variables named a are created?
-
How many variables named b are created?
-
How many variables named c are created?
-
How many variables named d are created?
(Hint: Each of the above four answers are different.)
(set! a 500)
(set! b 600)
(define counter1 (make-foo))
(define counter2 (make-foo))
(define counter3 (make-foo))
(counter1 'get-all) ; >>> TODO: (list )
(counter1 'incA)
(counter1 'incB)
(counter1 'incC)
(counter1 'incD)
(counter1 'get-all) ; >>> TODO: (list )
(counter2 'get-all) ; >>> TODO: (list )
|
-
O5 (10pts:
This problem and the next are really the crux of the project.)
Deferred evaluation:
O5 doesn't add any new syntax,
but it is a different evaluation model which
will give us more expressive semantics.
Use a new file/project for O5, separate from O0-O4.
There are two problems2
with the substitution approach used above:
we can't make recursive functions,
and it doesn't generalize to assigning to variables.
We solve these problems with deferred substitution:
-
When interpreting a program,
eval will take two arguments:
the program to interpret, and a set of (pre)existing bindings.
A binding is just an Id and its associated value;
a set of bindings (an environment)
might be implemented with an
association
list
or a java.util.Map<IdExpr,ValExpr>.
-
In this approach, function-application and let
no longer do any substitution —
instead,
each introduce (exactly one) new binding
and proceed recursively3.
-
Upon encountering an Id, we just look up its value
in our list of bindings.
(Recall that in O2, eval never actually
reached an Id;
in O4 it does.)
-
For full credit,
make sure that you can evaluate recursive functions.
-
You should not need to be doing any parse-tree-substitutions any more
(though you can if you want, to temporarily address the note below).
Note that in O6, no substitutions will be allowed.
-
Note:
anything stored in a Java Map
must have equals and hashcode which agree.
In our case, storing Ids,
be sure to have methods equals and hashcode
that both delegate their word to the String they contain4
Your test cases should include
a recursive function,
as well as the example below.
Also, since eval now takes an extra argument, that suggests three to four check-expects
with various environments (lists of bindings):
-
an empty environment;
-
an environment with no necessary bindings;
-
an environment where some of the bindings are needed;
-
an environment where some of the bindings will get shadowed in the expression.
A step sideways:
This algorithm as described lets us add recursive functions,
but it also doesn't handle some expressions that O4 did!
For example,
let make-adder := m -> (n -> (n add m))
in {{make-adder@3} @ 4} end;
gives an error "unbound identifier: m" if no substitution has been done,
however
{m -> (n -> (n add m)) @ 3 } @ 4}
does still work just fine(!).
The problem will be fixed in O6:
in the first example, {make-adder @ 3}
returns a function whose body involves m and n,
but not the binding of m to 3.
We'd need return the function and its bindings.
Note that this gives us dynamic scoping (which we'll mention in class):
let m := 100
in let addM := (x => (x add m))
in ((let m := 5 in {addM @ 3} end;)
add
(let m := 4 in {addM @ 3} end;))
end;
end;
|
evaluates to 15, not 206.
-
(15pts total) O6: Implement static scope.
- (5pts)
You'll want to
modify your function-structures so that
they include one extra piece of information:
the bindings in effect when the function was declared.
Be sure to make a careful suite of test-cases,
to test for various ways of capturing bindings in different closures.
(For example, consider the
lecture on ways of using
let* to effectively implement objects, classes, and inheritance.)
- (5pts)
When evaluating a function-application, use the bindings in effect
back when that function was declared, for its free variables.
(That set of bindings is called the functions “closure”.)
-
You should not be doing any substitution.
To think about:
Hmm, when we first parse our expression, we'll
create function-expressions,
but (since we're not eval'ing) we don't have any bindings
right then.
So, create it with a dummy environment (closure).
Only later, when we eval a function,
will we actually know about any bindings
(since that call to eval was given a list of bindings)….
subtlety:
This means that a function won't quite evaluate to itself anymore —
it'll evaluate to a struct that has the same parameter and body as the
original (parsed) structure,
but a fully fleshed-out, non-dummy closure.
- (5pts)
Note that getting recursive functions to work is a bit tricky,
since their environment (closure) needs to include themselves,
but when you're evaling a func-expr
you don't yet have its associated name, hmmm.
Just after the func-expr is created
then you'll want to go in and change its environment to
include itself.
The easiest way is to do this with mutation
(something like set-func-expr-env!
5).
Be sure to make a copy of your O5 project files before starting O6.
You shouldn't need any additional test cases for O6;
the tests for O0-O5 should suffice, although
any O5 examples depending on dynamic binding should now have
a new expected-result.
-
Further extra-credit options (of varying difficulty):
-
Add comments to your grammar and parser.
Make sure comments nest.
It's up to you whether or not you store comments in the
internal representation, or discard them entirely6.
-
Re-write the code for evaluating let
so that it simply transforms it into a function-application, and evaluates that.
-
Generalize functions to multiple arity,
and/or
generalize let so that it takes any
number of id/value pairs.
(Really this would be like scheme's
letrec,
since O4 allows recursion.)
-
If you want, modify the let syntax:
instead of
let id := Expr in Expr end;,
you can use
Id := Expr ; Expr ; .
(Note that this still represent declaring a new variable,
not mutating an existing one.)
Your programs now look like procedural programs.
-
Add mutation (i.e. assigning to Ids):
Id ← Expr;.
(If you want to use mutation in your underlying scheme implementation,
you can use set-first!
and
set-struct-field!7.
You can also think about how you might try implementing mutation in O6
without actually using mutation in your underlying program.)
-
Once we have assignment, add the equivalent of scheme's begin.
(You might use “{” and “}” to delimit
such a “block” of statements;
you now have implemented all the procedural concepts of
a Java or Python interpreter, including first-class functions!
And as seen in lecture,
you also could write
superficial transformations to get most of an object system as well.)
Here are some examples of the list predicates, for the
prolog list questions:
last([1,2,3], 3).
Yes
last([1,2,3], 4).
No
last([1,2,3], Y).
Y=3
last([], Y).
No
last(Y, 3).
Y=[3].
nextToLast([1,2,3], 2).
Yes
nextToLast([1,2,3], 3).
No
nextToLast([1,2,3], Y).
Y=2
nextToLast([1], Y).
No
nextToLast(Y, 3).
Y=[3, _h114], % does not have to be 114, 'course.
Y=[_h116, 3, _h114].
lastTwoReversed([1,2,3], Y).
Y=[3,2]
lastTwoReversed([1], Y).
No
reverseLastTwo([1,2,3,4], Y).
Y=[1,2,4,3]
reverseLastTwo([1,2], Y).
Y=[2,1]
reverseLastTwo([1], Y).
No
1We will talk about
this in lecture (let-over-lambda, to implement O.O.), but only briefly.
↩
2
A third issue, pointed out in
Programming Languages and Interpretation,
is that
evaluating deeply nested lets
is an O(n2) algorithm.
↩
3
Note that the list/map you recur with has the additional binding,
but that recursive call shouldn't add/modify
the list/map used at the top level.
Since java.util.Map is inherently mutable,
you'll want to make a new copy of that map before recurring.
↩
4
For example:
class Id {
String id;
public int hashcode() { return (this.id).hashCode(); } } // delegate to .id
public boolean equals( Object that ) {
if (this==that) return true;
else if (that==null) return false;
else if (that.getClass() != this.getClass()) return false;
else {
Id thatt = (Id) that;
// Whew, we can finally compare `this` and `thatt`:
return (this.id).equals(thatt.id); // delegate to .id's
}
// ...other methods...
}
|
↩5You'll have to use the keyword #:mutable
when you define-struct that structure,
to get the setter.
↩
6
You can think about which implementation you prefer, when
using a language:
when seeing the program's internal representation of your code
(when debugging, or seeing the compiled result of your code).
↩
7
Be default, scheme structs are non-mutable;
to make them mutable, you have to provide the keyword #:mutable
to define-struct.
↩
home—lectures—recipe—exams—hws—D2L—breeze (snow day)