RU beehive logo ITEC dept promo banner
ITEC 380
2020spring
ibarland

W6: environments

Due May.02 (Sat) 23:59
Submit: W6.rkt and W6-test.rkt (or, W6-java/*.java) on D2L . Include prolog-code in a block comment near the start of your file, and (perhaps only) the changed code from W4 (tagged “;>>>W5” and “;>>>W6”).

  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.)
    All of the predicates fail if the first argument is not a list. Some examples (test cases) are provided, below.

    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 V language implementation from previous homeworks (W4 sol'n). You may implement this homework in either Java or Racket (or another language, if you've cleared it with me). Copy your W0-W4 file/project to a new W51. Label each section of lines you change with a comment “;>>>W5” or “;>>>W6”. 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 W2–W4: (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 #y order-up 3} with an environment where y is bound to 7, and also evaluate #y order-up 3} with an environment where y is bound to 99.

  1. W5 : This problem and the next are really the crux of the project.)
    Deferred evaluation: W5 doesn't add any new syntax, but it is a different evaluation model which will give us more expressive semantics.

    Your test cases should include a recursive W5 function, as well as the addM example below. Here are just a few cases you might want to test:


  2. Discussing W5

    A step sideways: This W5 algorithm has improved on W4: we can now hope to handle recursive functions. But it’s also worse, because it now fails on some expressions that W4 got correct! For example,
    sweet make-adder mother of <^> m * |<^> n * #n order-up m}|
    pearl aye aye 3 captain |aye aye 4 captain make-adder|
    
    ; racket equivalent to the above W5:
    (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 W5, calling aye aye 4 captain 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 W6 with a slightly different approach, storing the environment-to-use inside the function-representation.

    Note that W5’s eval now gives us dynamic scoping:

    sweet     m
    mother of 100
    pearl     sweet     addM
              mother of <^> x * #x order-up m}
              pearl     # sweet m mother of 5 pearl aye aye 3 captain addM
                          order-up
                          sweet m mother of 4 pearl aye aye 3 captain addM
                        }
    
    
    
    ; the racket equivalent of the above W5:
    (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) as we might expect (and, prefer).

    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 (sweet m mother of 5? Or is it sweet m mother of 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.

    Note that even static-scope can give surprising results, if we have one variable shared by multiple closures, and then different functions mutate it. And in Java, the following won't even compile:

    for (int i=0; i<10; ++i)
    new Thread( () -> System.out.println("in thread #"+i) ).run(); 

    It gives you an error message saying mutation and shared state is bad for you (I'm paraphrasing).

Upshot: We’ll make W6, to reclaim static scope, and get what we expect!


Extra credit… but highly recommended W6: Implement static scope (closures). You shouldn't need any additional test cases for W6; the tests for W0-W5 should suffice, although one or two W5 examples depending on dynamic binding should now have a new expected-result. Further extra-credit options (of varying difficulty):

Mutation in Racket

If you want to use mutation in your racket-implementation, 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 W5 is not just W4 plus new code, but instead replaces some of the W4 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 (), 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 “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.      
7 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.