![]() |
![]() |
|
Due:
Submit:
H6.rkt and H6-test.rkt (or, H6-java/*.java) on D2L
.
Include prolog-code in a block comment near the start of your file,
and (perhaps only)
the changed code from H4 (tagged ;>>>H5
and ;>>>H6
).
You may use parts/all of H4-soln.
Prolog lists Write the following Prolog predicates. Do not use append/my-append. For full credit, give idiomatic Prolog (no singleton variables, and no = on the right-hand-side).
Note that the Prolog standard library 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.
We continue to build on the H language implementation from previous homeworks (H4-soln). You may implement this homework in either Java or Racket (or another language, if you've cleared it with me). Copy your H0-H4 file/project to a new H51. Label each section of lines you change with a comment “;>>>H5” or “;>>>H6”. You don't need to turn in any hardcopy of unchanged-code (but do submit a fully-working copy in the drop-box, including all necessary files).
There are two problems2
with the substitution approach used in H2–H4:
(i) we fundamentally can't create recursive functions,
and (ii) it’s hopeless should we want to add assigment to our language.
Less importantly, you might also have thought it's a bit inefficient (by a factor of two),
to do a substitution on an entire sub-tree, and then immediately re-walk through that same subtree
then eval it.
Can't we do those substitutions while we eval, “just in time”?
We solve these problems with deferred substitution:
Rather than substituting,
we’ll just remember what substitutions we want made,
and if we ever encounter an identifier then we look it
up in our set-of-deferred-substitutions — our environment.
So now we can both
evaluate add y to 3 with an environment where y is bound to 7,
and also
evaluate add y to 3 with an environment where y is bound to 99
H5 :
This problem and the next are really the crux of the entire project.
Deferred evaluation:
H5 doesn't add any new syntax,
but it is a different evaluation model which
will give us more expressive semantics.
standard dictionary types: In Java, the standard class java.util.Map<IdExpr,ValExpr> implements the Dictionary abstract-data-type. In Racket, two contenders are association lists(as seen on exam01), and immutable hash tables (here are examples of using both; hash-tables available to Student Languages via; (require "student-extras.rkt")).
Then, go back and write eval as a one-line wrapper around eval-with-env: eval will still take just one input (an Expr), and simply call eval-with-env, passing it an empty-environment.
This way, all your existing tests to eval can be unchanged, but you can add some unit-tests for eval-with-env to help figure out what that function needs to do with its environment.Here are just a few cases you might want to test:
This H5 algorithm has improved on H4: we can now at least hope to later handle recursive functions and re-assigning to variables (challenge-credit).
But it’s also worse, because H5 now fails on a few expressions that H4 got correct! For example,
substitute makeAdder with recipe using n : recipe using m: add n to m in use leftover 4 in use leftover 3 in makeAdder |
; the racket equivalent to the above H5: (let {[make-adder (lambda (m) (lambda (n) (+ m n)))]} ((make-adder 4) 3)) |
unbound identifier: mif no substitution has been done. The problem is that, in H5, calling use leftover 4 in make-adder returns a function whose body still includes m and n, but lacks the fact that we’d like it’s m to be bound to 3. One approach might be to have eval return both a value and an environment to use that value with. Upcoming solution: We’ll solve the problem in H6 with a slightly different approach, storing the environment-to-use inside the function-representation.
More generally, we find that H5’s eval is now giving us a different notion of binding, known as dynamic scoping:
substitute m with 100 in substitute returnM with recipe using x: m in substitute m with 5 in use leftover 3 in returnM ; the racket equivalent of the above H5: (let {[m 100]} (let {[returnM (lambda(x) m)]} (let {[m 5]} (returnM 3)))) |
Here's another example (w/ a slightly-less-trivial function):
substitute m with 100 in substitute addM with recipe using x: add x to m in add |
We say that H5 is using dynamic scope: the use of a variable (here, m) will refer to its most recent run-time definition! If m is free within a function addM, in dynamic scope you can't (statically) tell where its binding occurence is: are we inside ((let ([m 5]) …)? Or are we inside let ([m 100]) …)?). In general, a function far far way might introduce its own local m, and then call addM; the function addM will use that far-distant, “local” m!5. Dynamic scope has its limited place, but in general it is not how we want most of our variables scoped.
We want our usual static-scoping, where when you see a variable in the source-code, you can tell where its binding occurrence is. And we want to keep our notion of environments (rather than substitution), so that we can have recursive functions. So what do we do with a function that has m free in its body, and we want that to mean the m that is in the environment when the function is defined, not some possibly-other m that is in the environment when the function is called? We just need the function to remember what environment was being used when the function is defined, and use that environment for when we're evaluating the body! We call that environment the function's closure. This gives the effect you probably expected all along without thinking about it.
Still, beware mutation: Note that even static-scope can give surprising results, if we have one variable shared by multiple closures, and then different functions mutate it. First inspect the code of the following Javascript demo, then run it:
And in Java, the following won't even compile:It gives you an error message saying
for (int i=0; i<10; ++i) new Thread( () -> System.out.println("in thread #"+i) ).run();mutation and shared state is bad for your health(I'm paraphrasing).
Upshot: We’ll make H6, to reclaim static scope, and get what we expect!
A closure is a function plus the environment it was defined in.
One thing to note: Our func-exprs occur statically, at compile time (after calling parse). On the other hand, closures exist only at run-time (when calling eval).
(define-struct closure ([func-expr? f] [env? e])) |
Alternately: You could also modify H4's func-expr struct to have an additional field (it's closure), having this field be (say) #false at parse-time and then get its real value at eval-time. But in this posted solution, we'll make a whole new structure: it's more heavyweight, but makes our data better reflect with our meaning, and also help the type-system catch errors sooner (though still only at run-time).
To think about: Hmm, when we first parse our expression, we create function-expressions. But (since we're not eval'ing) we don't have any bindings right then. It's only later, if/when we eval-with-env a function-expression, that we will create closure. So functions no longer evaluate to themselves. However, a closure will evaluate to itself.
Note that closures only exist while a program is running (being eval'd), and expr->string is not usually called while a program runs. So if you want, I guess you could have expr->string just throw (error: 'expr->string "Internal error: I didn't think anybody would want to print a closure."). But for defensive programming, a super easy thing to do is to have a closure just print the same as its enclosed-function does.
And in fact there actually is a place where we might want to call expr->string from eval: if you encounter a run-time error, and want to print the offending expression as part of the error-message your interpreter creates.
Having just one check-expects for this is plausible, since it is just calling already-tested code.
Myself, I went ahead and had closures also print with their environment. Since that environment isn't something I needed to match something in our H6 grammar, I used racket's built-in string-formatting, which works for all types (incl. structs and lists): (format "the env: ~v" my-env)7 is all I needed to append on.
At this point, your code should run again (but fail the two new tests).
Note: Recall how eval'ing a func-apply was a lot like eval'ing a let-expr, except that the pieces we needed were distributed across two structs: the func-apply contained both the application's-argument and function (which in turn contained its param and body). This is reminiscent of that, except where we used to have an immediate func-expr, we now have a closure-structure which contains that func-expr and the environment we need.
I highly recommend: Sketch a concrete data-structure example, to get this right! Then turn your sketch into a check-expect.
Challenge/extra-credit Our scoping-rules for substitute, which are the same as lisp/scheme/racket's let, don't include the variable's own right-hand-side/initializer to be part of the scope, understandably. But that would preclude8 being able to write recursive functions. In lisp/scheme/racket, the let-variant letrec is the version with the scoping rule to allow defining recursive functions.
Getting recursive functions to work is a bit tricky: their environment (closure) needs to include its own name! That is, we’ll have eventually end up with a function-struct whose closure-field is a list containing the function-struct. That’s not a problem in racket, no more than it is in Java -- racket struct values are actually references-to-structs, just like in Java9. However, it is a place where you might want to use mutation (read on).
The tricky bit is that when
you're evaling a func-expr
you don't yet have its associated name, hmmm.
The “need” for mutation comes from the cyclical data-dependency:
a function-struct contains an environment which refers to that function-struct.
Using the shared form, to create cyclical data, removes the need for
you to do mutation, although internally it presumably uses mutation.
But you can also easily avoid the cyclical dependency, by using a level of indirection:
Just keep your function-structure as it was in H4 (does not contain its closure as a field),
but then make a function-with-env
structure which has two fields -- the pure function-struct
plus the environment to use when calling it;
eval will return/use this function-with-env type.
If you want to use mutation in your racket-implementation for the extra-credit, use Advanced Student language. This language level includes both: set! (to assign to variables), and set-struct-field! (to assign to struct fields). Note that these two actions are very different: one changes a local binding, the other mutates possibly-aliased values. That is why racket gives them different names; in Java assigning-to-field and assigning-to-local-variable can look identical (syntactically), despite giving rise to very different behavior.
Since the mutators return (void), and we still want(need) to return a (useful) value from every expression, we will use mutation inside a begin expression, which evaluates its sub-expressions in order, throwing away each resulting value (so: useful only if they had side-effects), and returns the result of the last expression:
(define times-called 0) (define (triplify-and-print n) (begin (printf "We use `begin` to call a function for a side-effect *and* return a value too.\n") (set! times-called (add1 times-called)) (printf "This function has been called ~a time~a.\n" times-called (if (= times-called 1) "" "s")) (* 3 n))) (triplify-and-print 5) (triplify-and-print 2) |
Btw, it’s worth noting that in full-racket (as opposed to advanced-student), begin is implicit in almost all forms (e.g. function-bodies and cond-branches).
Hmm, should closures really be considered an expr? I mean, a closure is not something in our grammar. However, it is a type of value, and it's something that may get eval'd or even ->stringd. So it is definitely a hack, to consider a closure as a type of value, and all values as expr?s. So be aware: Our type expr? is no longer exactly corresponding to our grammar's non-terminal <Expr>!
An alternate solution would to make a new type (or/c expr? closure?), and use that as appropriate in our signatures as appropriate.
↩Precludeis a strong word; it turns out it is actually possible to define recursive functions w/o even naming them!! See the lambda calculus ↩
This page licensed CC-BY 4.0 Ian Barland Page last generated | Please mail any suggestions (incl. typos, broken links) to ibarland ![]() |