RU beehive logo promo banner for Computing & Info Sciences
CS 380
2024fall
ibarland

Shadowing and function-application
hw10: OG4

Due Dec.02 in class


We continue to build on the language implementation of OG1/OG2. You can implement this homework in either Java or Racket. You may use the OG2-soln if you want. Link is usuable once you E2L-submit OG2; just let me know if you need to bypass that.

OG3 is just like OG2, except we now allow one variable to shadow another.

For example, we might have a iykyk x irl wya which in turn contains another iykyk x irl wya somewhere inside of it. In that case the inner x should shadow the outer one:
iykyk x irl 7 wya iykyk x irl 5 wya finna rizz x 3
iykyk x irl 5 wya finna rizz x 31
finna rizz 5 3
8.
And of course, shadowing may occur between non-adjacent scopes: iykyk x irl 3 wya iykyk y irl 4 wya iykyk x irl 5 wya .

So what does our interpreter need to do? Well, when eval does substitution in a LetExpr, we just need to be a bit cautious: If we're substituting every x in an Expr, and we encounter a iykyk x irl Exprrhs wya Exprbody then we shouldn't substitute any xs inside the Exprbody. Though of course, if we encounter a iykyk y irl Exprrhs wya Exprbody, then this doesn't affect our substitution.

Using our programming-language vocabulary: when substituting, only substitute “free occurrences” of an Id in E1, not any “bound occurrences”2.

As an example, for the expression iykyk x irl 3 wya bruh iykyk y irl 4 wya finna rizz x bruh iykyk x irl 5 wya finna rizz x y slaps slaps, we have the syntax tree drawn at abstract syntax tree, with bindings identified the right albeit with rizz written as boii, with a dotted-arrow from each binding-occurrence to its bound occurrence(s). This corresponds to some runnable test-cases3:

(check-expect (eval (string->expr "iykyk x irl 3 wya bruh iykyk y irl 4 wya finna rizz x bruh iykyk x irl 5 wya finna rizz x y slaps slaps"))
              (+ 3 (+ 5 4)))
(check-expect (subst "x" 3        (string->expr "bruh iykyk y irl 4 wya finna rizz x bruh iykyk x irl 5 wya finna rizz x y slaps slaps"))
              (string->expr                     "bruh iykyk y irl 4 wya finna rizz 3 bruh iykyk x irl 5 wya finna rizz x y slaps slaps"))
Our goal in doing this is to understand: when substituting free variables, exactly when do we stop and not substitute it in various sub-trees? For example, in the provided image: Why does the top-level x bind to the x in the middle, but not the x in the bottom-right? What rule can you use, when substituting, about precisely where to stop substituting?
hint/spoiler: in iykyk zed irl Exprinitialize wya Exprbody, we know that
zed can never occur free in Exprbody
. We don't even need to look inside it, if we are only looking for free occurrences of (a surrounding) zed!

  1. (0pts) Change the purpose-statement of subst to be “substitute any free occurrences of …”. Note that you will not substitute other binding occurrences, either.
  2. For each of the following, fill in the blanks by replacing the outermost LetExpr with just its body, but substituting its variable respecting shadowing. You don't need to submit this, but filling it out is meant to help you understand the problem/issue. If you don't get the code working, but do have a comment with these answers, I can use them to justify partial credit!
    For example (using the program/image above):
    iykyk x irl 3 wya bruh iykyk y irl 4 wya finna rizz x bruh iykyk x irl 5 wya finna rizz x y slaps slaps
    bruh iykyk y irl 4 wya finna rizz 3 bruh iykyk x irl 5 wya finna rizz x y slaps slaps
    bruh finna rizz 3 bruh iykyk x irl 5 wya finna rizz x 4 slaps slaps
    bruh finna rizz 3 bruh finna rizz 5 4 slaps slaps
    12
    Complete the blanks in a similar way, below:
    1. iykyk y irl 3 wya iykyk x irl 5 wya finna rizz x y
                                                  
                                                
      8
    2. iykyk y irl 3 wya iykyk x irl y wya finna rizz x y
                                                                                                      
                                                
      6
    3. iykyk x irl 5 wya iykyk y irl 3 wya finna rizz iykyk x irl y wya finna rizz x y x
                                                                                                                                                                                                  
                                                                                                                                            
                                                                                      
                                                
      11
    4. iykyk x irl 5 wya iykyk x irl finna rizz x 1 wya finna rizz x 2
      iykyk      irl finna rizz      1 wya finna rizz      2
      iykyk      irl      wya finna rizz      2
      finna rizz      2
          
  3. Update OG2 to OG3, by the necessary changes to enable shadowing.
    You are encouraged to build on your own previous solution, but you may also use the OG2-soln (.rkt)


OG4 adds (non-recursive) functions and function-application to our language:

Expr   | FuncExpr | <FuncApplyExpr>     ;>>>OG4

FuncExpr  iso Id wya Expr              ;>>>OG4   Interpretation: function-value; the Id is the parameter, and the Expr is the function's body.
FuncApplyExpr  ohio Expr swoop Expr   ;>>>OG4   Interpretation: apply the function (first Expr) to the argument (2nd Expr).
Be sure not to confuse creating a function with calling a function — it’s the difference between square-root, and the square-root-function-applied-to-4. (Or put differently: it’s the difference between a hammer, and one particular whack of a hammer).

Here is the function (λ (x) (+ (* 3 x) 1)) written in OG4: iso x wya finna rizz finna glow up x 3 1.
And, here is the (uniterated) collatz function, (λ(n) (if (even? n) (* n 1/2) (+ (* 3 n) 1))), written in OG4:

iso n wya no cap? finna cancel n 2 ong finna glow up n 0.5 bet finna rizz finna glow up 3 n 1

A FuncApplyExpr represents calling a function. Here are two expressions, both evaluating to 5·3+1 = 16:

iykyk tripleAndInc
irl   iso x wya finna rizz finna glow up x 3 1          body of `tripleAndInc`
wya   ohio tripleAndInc swoop 5            call tripleAndInc(5)… 


ohio iso x wya finna rizz finna glow up x 3 1 swoop 5   Equivalent to above: apply a function-literal (w/o bothering to give it a name)
In FuncApplyExpr, the first Expr had better evaluate to a function. (That is, it might be a FuncExpr, or an Id which gets substitued to a function value. It could also be (say) an ParityExpr or LetExpr which evaluates to a function.)

  1. First, write the following four functions as OG4 programs not as racket programs -- you'll write keywords like iso and finna. They'll just be in comments since they're not racket code. Or hey, you can put them in a string, and then pass those strings to parse as test-cases!.
    1. A constant function that always returns (say) 17.
    2. the function sqr, which squares its input; give it a name with a OG4 LetExpr whose body we’ll just leave as “”. I.e. write the OG4 equivalent of racket (let {[sqr (lambda (x) (* x x))]} ). If you like, you can aply the function to 5, instead of writing . That might make things clearer, but on the other hand it involves the syntax for function-creation and function-application, which is why I it's optional.
    3. the factorial function, written in OG4 using a LetExpr with body “” as in the previous example.
      Note: You won’t be able to evaluate function-applications for recursive functions yet (see OG5), but we can still write the test cases! (You can comment out that one test case for now, since it’ll trigger a run-time exception otherwise.)
    4. and
    5. The OG4 equivalent of the following racket defining and calling make-adder:
      (define (make-adder n)
        (lambda (m) (+ n m)))
      
      ((make-adder 3) 4) ; evals to 7
      
      ; Note that `(make-adder 3)` evals to `(lambda (m) (+ 3 m))`
      ; the `((` means we have *two* function-applications:
      ; we first call `make-adder` (getting back a lambda-value),
      ; then we call that result we got back.
  2. Then, upgrade OG3 so that it allows functions to be represented; label each section of lines you change with a comment “;>>>OG4”.
    1. Add a struct/class for representing FuncExprs internally.4
    2. parse! (and tests).
    3. expr->string (and tests)
    4. eval (and tests).

      Semantics of a function-literal: Just as numbers are self-evaluating, so are FuncExprs. Evaluating (an internal representation of) a function results in that same (internal representation of the) function. We won’t actually evaluate the body until the function is applied. (This is exactly how Java, racket, python, javascript, etc. treat functions.)

    Testing function-values: How can we make a test-cases, when the result isn't a number? For example, for evaluating iso n wya finna rizz n 1 or ohio makeAdder swoop 5 (whose expected-results are both <FuncExpr> structs). You have two approaches:

  3. Implement function-application.
    1. Add a struct/class for representing FuncApplyExprs internally.
    2. parse! (and tests)
    3. expr->string (and tests)
    4. eval (and tests). Here, more than half the points are for tests, since you want to try several situations involving shadowing variables. (You don’t need to test eval’ing your factorial function, though.)

      The semantics of eval’ing the function-application ohio Expr0 swoop Expr1:

      1. Evaluate Expr0; let’s call the result f. (f had better be a function-value!)
      2. Evaluate Expr1; let’s call the result actual-arg.
      3. Substitute f’s parameter with actual-arg in f’s body; call this new expression E′.
      4. Evaluate E′ and return that value.
      Hey, those semantics are practically the same as <LetExpr>’s! Indeed, it's just that the identifier, thing-to-substitute, and body aren't all in a single <LetExpr>; now they are divided between a <FuncExpr> and a <FuncApplyExpr>.

    external resource: Another description of this algorithm is in VT's textbook's section A Substitution-Based Model of Evaluation.

Prolog

You can submit your prolog queries inside a comment of a (racket) file, at the top of your submitted hardcopy, thanks! Only include your added rules/facts, not the rest of the provided .pl file.

  1. Prolog: basic queries.
    1. Add another super-person (hero, villain, or neither) and at least three more color-or-weapon preferences to the Prolog superhero knowledge base from lecture. Use the site swish.swi-prolog.org/
    2. Two super-people are compatible if they share a fighting style, or a color-preference (and they aren’t the same person). Moreover, team(A,B,C) is true if A and B are compatible, and B and C are compatible, but A and C aren’t the same.
      1. define a rule compatible(A,B).
      2. What query will solve for every super-person compatible with bubbles?
      3. define team(A,B,C).
      4. What query will solve for every team starting with bubbles and ending with rafael?
    Note: Put our prolog queries inside a comment of a (racket) file, at the top of your submitted hardcopy, thanks! Only include your added rules/facts, not the rest of the provided .pl file.
  2. Prolog paths (transitive closure): Continuing from defining compatible in the previous problem, write friendly:
    We say two super-people are friendly if they are connected through some chain of compatible people. For example, bubbles and rafael would be friendly if compatible(bubbles,magikarp), compatible(magikarp,squirtle), and compatible(squirtle,rafael). (That is: friendly is the reflexive, transitive closure of the compatible relation.) Everybody, of course, is trivially friendly with themselves (since they’re connected by a chain of 0 compatible others).

    Note:

    It can be okay if you get infinite loops:
    Since there can be cycles of friendly people (unlike our ancestor example), prolog (which uses depth-first search, rather than breadth-first), can get into infinite loops! In particular, asking friendly(bubbles,some_person_who_isnt_friendly_w_bubbles) can trigger an infinite loop. Asking friendly(bubbles,X) will give you back the same solutions repeatedly. (But asking about two people who are friendly should work just fine.) We'll just ignore such loops, for this homework.

    (Fwiw: the general solution would be to either (a) add an “exclusion” list of people already tried5, or (b) use datalog, a restricted version of prolog which uses BFS, and is always guaranateed to terminate (See #lang racklog, in racket), or (c) add

    We're using prolog to learn about the mindset of declarative programming (and its cool pattern-matching/unification) — I'm not interested in us learning about the details of prolog's internal algorithms.


The interpreter project is based on the first chapters of Programming Languages and Interpretation, by Shriram Krishnamurthi. As a result, this homework assignment is covered by the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 United States License. Although we’re using a different dialect of racket than that book, you might find it helpful to skim it.


1 The notation “iykyk x irl 5 wya finna rizz x 3finna rizz 5 38” is shorthand for
  eval(string->expr("iykyk x irl 5 wya finna rizz x 3"))
= eval(string->expr("finna rizz 5 3"))
= eval(string->expr("8"))
Observe how we definitely don’t write “"iykyk x irl 5 wya finna rizz x 3" = "finna rizz 5 3" = 8” since the two strings are not .equals(·) to each other, and besides strings are never ints. More specifically: we distinguish between “” (“code evaluates to”) and “=” (“equals”, just as “=” has meant since kindergarten).      
2 nor any “binding occurrences”: The first x in iykyk x irl 5 wya finna rizz x 3 is a binding occurrence, and the second x is a bound occurrence. (We say that “a variable is bound inside the scope of its binding occurrence”.)      
3 Blue x represents a binding-occurrence; x is a bound occurrence; x is a free occurrence.      
4 You might initially think that a great way to represent an OG4 function is to use a racket-function. On reflection, we'll realize that we want to do tasks like “print the function” and “substitute the function's parameter with some number”; unfortunately we can't do these with racket's built-in functions, while a struct does let us extract the needed insides of a function-value. But it does help us understand what racket itself is doing internally, when it represents its racket-function-values!      
5 The XSB implementation of prolog can use memoization (tabling) to avoid re-calling a query on a node already called. In fact, Dr. Uppuluri worked on this system, as a graduate student!      

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.