RU beehive logo ITEC dept promo banner
ITEC 380
2021fall
ibarland

environments and closures

Due Dec.04 (Sat) 23:59
Submit: Z5.rkt and Z5-test.rkt (or, Z5-java/*.java) on D2L , and hardcopy under Dr. Barland’s door by Monday 17:00. Include prolog-code in a block comment near the start of your file, and (perhaps only) the changed code from Z4 (tagged ;>>>Z5 and, if completed, ;>>>Z6).

  1. Prolog lists

    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).

    1. 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.)
    2. 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.)
    3. lastTwoReversed(List, ListOf2) which succeeds exactly when ListOf2 contains the last and the second-to-last item of List (in that order, and nothing else).
    4. reverseLastTwo(List, NewList) succeeds exactly when NewList is exactly the same as List except that the last two items have been reversed. (This rule will fail if either input has fewer than two items.)
    Some examples follow. All of the predicates fail if the first argument is not a list.
    last([1,2,3], 3). true. last([1,2,3], 4). false. last([1,2,3], Y). Y=3. last([], Y). false. last(Y, 3). Y=[3], …
    lastTwoReversed([1,2,3], Y). Y=[3,2]. lastTwoReversed([1], Y). false.
    nextToLast([1,2,3], 2). true. nextToLast([1,2,3], 3). false. nextToLast([1,2,3], Y). Y=2. nextToLast([1], Y). false. nextToLast(Y, 3). Y=[3, _h114], % does not have to be 114, 'course. Y=[_h116, 3, _h114], …
    reverseLastTwo([1,2,3,4], Y). Y=[1,2,4,3]. reverseLastTwo([1,2], Y). Y=[2,1]. reverseLastTwo([1], Y). false.

    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.

Environments and Closures

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, if completed,;>>>Z6”. 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).

Shortcomings of substitution

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.

  1. 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.

    Here are just a few cases you might want to test:


Discussing Z5

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,
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))
gives an error unbound identifier: m if 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))))
evaluates to 5, not 100 as we might expect/prefer.

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)))))
evaluates to 15=(3+5)+(3+4), not 206=(3+100)+(3+100).

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!


OPTIONAL

Completing Z6 is optional/challenge. It's a great additional step, and really culminates the project well, but we got a half-week behind on homeworks earlier in the semester, so it's not required.

Z6: Functions have Closures

    1. Make a backup copy of your working Z5, since we will temporarily break the code.
    2. Make test-cases for the above problems, with the expected-outcome using static scoping (that is, the same as racket returns). The above two test-cases will suffice, or you can think up more. On Friday’s lecture, we'll see on ways of using let* to effectively implement objects, classes, and inheritance.)
    3. Modify your function-structures so that they include one extra piece of information: the environment (bindings) in effect when the function was declared. This is the function’s closure.

      This might (or might not) entail updating some of your test-cases, if they called make-func.

    4. 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.

    5. Update parse, so that make-func provides an initial value of (say) #f for the closure-field.

      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).

    6. Now that we've thought about it: Update eval-with-env so that when it encounters a function-definition, it returns a function whose closure is the current-environment (instead of the dummy-value #f that parse! had used).
    7. Finally update eval-with-env so that when it encounters a function-application, it uses that function's closure (i.e., the environment in effect back when that function was declared) for any free variables in its body. Of course, you also still need to include the parameter's binding, just like you did in Z5.

  1. Everything below here is extra-credit, after completing the above. I do recommend reading it over as food-for-thought, even if you don't plan on implementing any of it.
  2. 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 name only gets established outside the function-definition, in the surrounding Z6 LetExpr being eval’d. So after the let-statement has finished evaling its “right-hand-side” value (which turns out to be a function, including its closure), then you’ll want to further reach in and modify that closure to include one additional ID/value pair. You can, if you like, use the struct-setter (something like “set-func-closure!”); see mutation in racket below.

    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.

  3. Further challenge/extra-credit options (of varying difficulty):
      item{Allow ! 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.}
    1. Add block-comments to your language; Make sure comments nest. It’s up to you whether or not you store comments in the internal representation, or discard them entirely8. You might want modify your grammar, or consider a multi-phase approach to parsing.
    2. Re-write the code for evaluating let so that it simply transforms it into a function-application, and evaluates that.
    3. 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 Z6 allows recursion.)
    4. If you want, modify the LetExpr syntax: instead of let id let Expr in {: Expr :}, 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. Note that parsing is more difficult: after reading an Id, you have to check for whether it’s followed by = or not. You should satisfy yourself that this grammar not ambiguous.)
    5. Add mutation (i.e. assigning to Ids): IdExpr;. See mutation in racket below.
    6. 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.)

  4. Mutation in Racket

    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).


1 Presumably you do this for each new homework, but it’s a particularly good idea for this one, since Z5 is not just Z4 plus new code, but instead replaces some of the Z4 code (subst) with a different approach.      
2 A third issue, pointed out in Programming Languages and Interpretation, is that evaluating deeply nested lets is an O(n2) algorithm.      
3 In DrRacket: Click Check Syntax icon: check-mark with magnifying glass and then right-click on the definition of eval, and choose Rename eval.      
4 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. If using Java, java.util.Map is inherently mutable, so you’ll want to make a new copy of that Map when recurring.      
5 Early Lisp (1965) included dynamic scope; this was soon recognized as less-than-desirable, and keywords for creating statically-scoped variables were introduced. Similarly the first versions of Clojure’s (2007) var were dynamic with a keyword to make them statically-scoped; this was changed in 2010 to be static-by-default (with a keyword provided to create dynamically-scoped variables). Here’s a (Clojure-oriented) blog post “Perils of Dynamic Scope”.      
6 Preclude is a strong word; it turns out it is actually possible to define recursive functions w/o even naming them!! See the lambda calculus      
7 “What, racket uses references-to-structs instead of actual structs? Why didn't you tell us that earlier, Barland?” Because in the absence of mutation, no program’s results can distinguish between having an object vs a reference-to-object-which-is-always-dereferenced. So it was like a religious-opinion difference, until getting mutation in Advanced Student.      
8 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).      

logo for creative commons by-attribution license
This page licensed CC-BY 4.0 Ian Barland
Page last generated
Please mail any suggestions
(incl. typos, broken links)
to ibarlandradford.edu
Rendered by Racket.