![]() |
![]() |
|
Due
Submit:
Z5.rkt and Z5-test.rkt (or, Z5-java/*.java) on D2L
;>>>Z5
and, if completed, ;>>>Z6
).
Write the following Prolog predicates. Do not use 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 X language implementation from previous homeworks
(Z4-soln).
You may implement this homework in either Java or Racket (or another language, if you've cleared it with me).
Copy your Z0-Z4 file/project to a new Z51.
Label each section of lines you change with a comment
“;>>>Z5”
or
There are two problems2
with the substitution approach used in Z2–Z4:
(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
evaluate [add y 3] with an environment where y is bound to 7,
and also
evaluate [add y 3] with an environment where y is bound to 99.
Z5 :
This problem and the next are really the crux of the project.)
Deferred evaluation:
Z5 doesn't add any new syntax,
but it is a different evaluation model which
will give us more expressive semantics.
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:
(string->expr "fun n => zero n ? 1 : [mul n <pass [sub n 1] |> !>]") |
A step sideways: This Z5 algorithm has improved on Z4: we can now at least hope to handle recursive functions (challenge-credit). But it’s also worse, because it now fails on some expressions that Z4 got correct! For example,gives an error
let make-adder := fun m => <fun n => [add n m]> in pass 3 |> <pass 4 |> make-adder> ; racket equivalent to the above Z5: (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 Z5, calling pass 4 |> 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. We’ll solve the problem in Z6 with a slightly different approach, storing the environment-to-use inside the function-representation.
Note that Z5’s eval now gives us dynamic scoping:
let m := 100 in let returnM := fun x => m in let m := 5 in pass 3 |> returnM ; the racket equivalent(?) of the above Z5: (let {[m 100]} (let {[returnM (lambda(x) m)]} (let {[m 5]} (returnM 3)))) |
Here's another example (w/ a slightly-less-trivial function):
let m := 100 in let addM := fun x => [add x m] in [ add let m := 5 in pass 3 |> addM let m := 4 in pass 3 |> addM ] ; The racket equivalent(?) of the above Z5 expr: (let {[m 100]} (let {[addM (lambda (x) (+ x m))]} (+ (let {[m 5]} (addM 3)) (let {[m 4]} (addM 3))))) |
In dynamic scope, the use of a free variable (here, m) will refer to its most recent run-time definition! If m is free within a function addM, you can't tell where its binding occurence is (let m <- 5? Or is it let m <- 100?). In general, a function far far way might introduce its own, local m and call addM; the function addM will use that far-distant, “local” m!5.
Upshot: We’ll make Z6, to reclaim static scope, and get what we expect!
This might (or might not) entail updating some of your test-cases, if they called make-func.
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, initially create it with an dummy environment (a sentinel value like #f).
Only later, when we eval-with-env a function, will we actually know about any bindings (since that call to eval-with-env 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 fleshed-out, non-dummy closure.
Note that toString needs no updating, nor does subst since we got rid of that in Z5.
At this point, your code should run again (but fail the two new tests).
Challenge/extra-credit Our scoping-rules for let, 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 preclude6 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 Java7. 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 Z4 (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.
!to occur next to any other tokens.
add! krunch!!While this fits naturally in parse!, the test-harness's test-expr->string will also need updating. Conceivably you could go deeper and modify the underlying scanner to just ignore that token, but feels less proper to me. But admittedly, that approach would only need to modify one place in the code.}
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, which is why racket gives them separate 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:
(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).
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 ![]() |