CSC 330 Lecture Notes Week 9

CSC 330 Lecture Notes Week 9
More on Functional Programming and Lisp


  1. Completion of topics from the Lisp Primer.

  2. Discussion of Assignment 6.
    1. What are you doing here?
      1. You're writing a Lisp interpreter very much like the early implementors of Lisp did, about 45 years ago.
      2. This is foundational work that led to the development of today's compilers and interpreters.
      3. And most interestingly, the way interpreters are written today is fundamentally the same as how they were written way back then.
    2. The top-level execution environment for your interpreter -- the read-xeval-print-loop (in test-files/read-xeval- print.l)
             ; This is an "x" version of Lisp's read-eval-print loop.  When the function
             ; read-xeval-print-loop is run, it will interactively input expr's from
             ; the terminal and hand them over to xeval.  Note that memory is defined as
             ; a prog var, that is sent automatically as a parameter to xeval.
             ;
             (defun read-xeval-print-loop ()
               (prog (memory result)
                 (setq memory '(nil))
                 loop
                     (princ "X>")
                     (setq result (xeval (read) memory))
                     (princ (car result))
                     (setq memory (cadr result))
                     (terpri)(terpri)
                     (go loop)
               )
             )
             
      1. Notice that it's grossly imperative, with the use of setq's and goto's.
        1. The use of imperative constructs is limited to the top-level loop; the interpreter itself, i.e., the xeval function, is entirely functional in its behavior.
        2. In particular, xeval does all of its work through parameter passing and value returning, without using any global variables.
      2. Notice that the return value of xeval is a pair of the form
        (computed-value-of-expr possibly-updated-memory)
      3. By providing the memory as an explicit input parameter and returning it as part of the return value, xeval does not require a global store, in contrast to the normal Lisp eval.
    3. A core issue in Assignment 6 is the structure of memory.
      1. The good-news/bad-news situation with Lisp is that due to weak typing, there is no explicit declaration of the memory structure anywhere in the template.
      2. The good news is that weak typing allows high flexibility in how data structures are manipulated.
      3. The bad news is that in order to determine what the structure of data is, one has to examine the program code closely.
      4. In the case of Assignment 6, an important clue for how memory is structured is in memory dump dump shown in the file xeval-test-demo.

  3. Details of memory layout.
    1. Fundamentally, the memory is a list of bindings.
    2. The general form of a binding is
             ( name value )
             
    3. For Lisp, bindings can be subdivided into two categories:
      1. A variable binding is a pair of the form:
               ( var-name data-value )
               
      2. A function binding is a triple of the form:
               ( function-name formal-parms function-body )
               
    4. Note that we distinguish between variable versus function bindings by their lengths -- two and three, respectively.
      1. This is a much simpler and more abstract way of doing things than in the EJay interpreter of Assignment 4.
      2. There we had to define separate extensions of SymbolTableEntry, do casting when necessary, and mess around with a lot of strongly-typed structure.
      3. Here the difference between VariableEntry versus FunctionEntry is just how many elements there are in the list representation of each.
      4. Further, we are using the simple list-style bindings to represent both environment information (symbol table) and data store information (memory).
      5. E.g., the binding '(x 4) can be looked up for x as in a symbol table, and evaluated to 4 as in a memory.
    5. In the Lisp of Assignment 6, bindings can be created and modified in three ways:
      1. Variable bindings are created and modified by (xsetq x v) as follows:
        1. If there is no existing binding for x on the memory, add the new binding (x v) to the end of the memory.
        2. If there is an existing binding for x on the memory, change the value of the binding to v.
      2. Function bindings are created and modified by (xdefun f parms body) in precisely the same way as variable bindings with setq; namely
        1. If there is no existing binding for f on the memory, add the new binding (f parms body) to the end of the memory, where parms is a list of formal parameters of the form ( p1 ... pn ).
        2. If there is an existing binding for f in memory, change the value of the binding to parms body.
      3. Function call bindings (a.k.a., activation records) are created and removed by function calls of the form (f a1 ... an)
        1. Before the function body is evaluated, a new binding is created for each ai, of the form
                 ( pi ai )
                 
          and these bindings are added to the end of the memory.
        2. After the function body has been evaluated, the activation bindings are removed, by restoring the memory to its state before the bindings were added.
    6. In all cases above, the addition, removal, and search for bindings is done in a LIFO discipline
      1. Whenever a new binding is made, it is added to the end of the memory.
      2. Whenever a binding is searched for, the search starts from the end on which the most recent addition was made.
      3. And note well the phrase from above: "... by restoring the memory to its state before the bindings were added".
        1. This means that any new bindings made by setq or defun during the course of function body evaluation are non-permanent.
        2. I.e., such bindings effectively become part of the activation record, and are removed after function evaluation has completed.
    7. Your memory implementation for Assignment 6 does not have to have a physically identical structure to that shown in xeval-test-demo.
      1. However, whatever specific implementation of memory you use, you must get the same results for the output values as in xeval-test-demo.
      2. That is, your memory dump can look different than mine, but the value results must be the same.
    8. It's worth noting that this memory model does to reflect exactly the scoping rules of Common Lisp, however it's the memory model we're using for Assignment 6 since it's simple and straightforward.
      1. What we're doing here is co-mingling the static pool and stack into a single piece of memory.
      2. When global variables are bound at the top-level of the interpreter, they go in the top part of memory, which is effectively the static pool.
      3. When functions are called, activation records are pushed onto the stack, with the top of stack being at the bottom of the current static pool.
      4. These two aspects of memory management are the same as in Common Lisp and the EJay interpreter of Assignment 4.
      5. What's different here compared to Common Lisp (and EJay) are the following points:
        1. When new bindings are made inside of a function, they go in the function's activation record, not in the static pool.
        2. When we look for the current value of a variable, we don't look separately in the stack then in the static pool, but instead just search all the way up the stack, which ends in the static pool.
      6. The reason we're doing things this way that it's the simplest form of memory, and allows us to treat the stack and static pool as one uniform structure.
      7. We'll discuss the ramifications of this simplified memory model next week.

  4. Discussion of Assignment 5 solutions (handout coming on Wednesday).

  5. Summary of the types of languages Lisp can be
    1. Pure Applicative -- disallow setq entirely, as well as all destructive list operations, and all imperative control constructs (the control constructs are essentially useless without assignment anyway).
    2. Single-Assignment -- allow let, but still no setq, no destructive's, and no other imperative features.
    3. Large-Grain Applicative -- allow setq, and imperative control, but only inside functions; i.e., no free variables in functions, and still no destructive's.
    4. Nice Imperative -- allow general setq (both local and global), and imperative control, but still no destructive's.
    5. Nasty Imperative -- allow it all, including destructive's.




index | lectures | handouts | assignments | examples | doc | solutions | bin