CSCI 330: Spring Midterm 2024 Sample Solutions


Question 1: tail recursion [8]

Describe a general process by which a tail recursive function can be developed from an iterative (loop-based) one, and provide a short code example to illustrate your approach.

Sample solution

Each of the local variables in the original iterative solution becomes
   an optional parameter in the tail recursive one,
   with a default value that matches the initialization value in the original
The base case/termination condition for the tail recursive solution is
   the opposite of the continuation condition for the original loop,
The return value for the base case matches the post-loop return value
   from the original
The general case for the tail recursive version is simply a recursive call,
   but the value passed for each parameter represents the collected modifications
   made to the corresponding variable in the body of the original loop

int f(int N)                                 int f(int N, int i=1, int sum=0)
{                                            {
   int sum = 0;                                 if (i >= N) {
   for (int i = 1; i < N; i++) {                   return sum;
       sum = sum + i;                           } else {
   }                                               return f(N, i+1, sum+i);
   return sum;                                  }
}                                            }


Question 2: list implementations [8]

Breifly outline the way common lisp implements lists, then discuss the impact this has on list copy operations (shallow/deep) and how that in turn might impact recursive functions passing lists as parameters.

Sample solution

List implements lists as a linked list of cells, each with two elements:
    the cell data (the car)
    a reference to the next cell (the cdr)
A list variable is actually a reference/pointer to the first cell

Shallow copies are performed whenever we use a defvar, setf, or pass a list as
   a parameter to another function.  These simply copy/pass the pointer/reference,
   rather than building an entirely separate duplicate of the original list.
(The latter would be deep copies: specific functions exist to create deep copies
    when desired, but these must be explicitly called if needed.)

When making a shallow copy, only the pointer value needs to be replicated,
   which can be performed in constant time and takes constant space.
A deep copy, on the other hand, would require creating and storing an entirely
   separate duplicate version, requiring time and space proportional to the
   size of the original list.

The effect is particularly noticable in recursive functions that pass lists as
   parameters, since with deep copy each recursive call would need to
   duplicate and store the list.

In terms of behaviour, shallow copies introduce the possibility of side effects:
   if multiple variables or parameters hold pointers/references to the same
   stored list then if any of them change the content of the list then the
   impact is observable by all of them: they're all accessing the same shared data.


Question 3: let-over-lambda [8]

(i) Discuss how let-over-lambda could be considered to provide object-oriented-like constructs in common lisp.

(ii) Could let-over-lambda work without the use of higher order functions? Justify your answer.

Sample solution

(i) The variables in the let block are uniquely associated with the specific
    call to the construction function and the lambda dispatcher it returns.
    This gives each dispatcher control over its own stored state, much like
    an object has control over its own collection of stored fields.

    The lambda dispatcher allows a caller to request access to or manipulation
    of the stored state through a collection of fixed commands/messages, just as
    an OO system uses messages/methods to request access to/manipulation of the
    stored state of an object.

    The construction function determines how the stored state is initially set,
    based on the parameters passed to it.  This acts much like the constructor
    for a traditional object.

    In theory, we could tweak our construction function to accept other
    construction functions as parameters: e.g. if we had a 'buildVehicle'
    construction function we could pass it (along with whatever other data)
    to a 'buildBicycle' construction function.  The buildBicycle could
    pull the necessary components from the buildVehicle to allow a form
    of inheritance.  (It wouldn't be pretty, but it could be done.)


(ii) By definition, let-over-lambda returns a lambda function that controls
     access to the stored internal state.  That lambda function cannot be run,
     and hence the stored state cannot be accessed, without the use of some
     higher order function.


Question 4: pure functional languages [8]

Discuss the various ways in which let-over-lambda would not be regarded as functionally pure.

Sample solution

The most glaring example of functional impurity is in the necessity of stored state:
   The idea of stored state is antithetical to the concept of pure functional programming,
   but LOL's entire scheme for representing objects revolves around having a unique
   and persistent stored state associated with each individual object/dispatcher
   (coming from the let block within the original construction function).

Generally speaking, this stored state is intended to be manipulated through
   the commands sent to the dispatcher and the resulting code it runs on the
   let block variables.  This means that the dispatcher function must have
   side effects, which is again in direct opposition to a pure functional approach.

I would probably argue that the capability for let blocks to support sequencing
   is not a direct issue with the LOL approach vs functional purity, as there is
   no reason the LOL programmer has to adopt an impure approach here:
   the body of the let block could simply be a single function call whose return
   value is the constructed lambda function.


Question 5: macros [8]

The recursive variadic macro below is intended to operate somewhat like the reduce function, but reducing across the set of arguments passed to the function instead of reducing across the contents of a list.

(defmacro reduceArgs (f x1 x2 &rest others)
   (if (null others) `(,f ,x1 ,x2)
       `(reduceArgs ,f (,f ,x1 ,x2) ,@others)))

(reduceArgs 'list 3 4) is intended to expand to (LIST 3 4) (reduceArgs 'list 3 4 5) is intended to expand to (LIST (LIST 3 4) 5) (reduceArgs 'list 3 4 5 6) is intended to expand to (LIST (LIST (LIST 3 4) 5) 6) etc

Assuming an appropriate function was passed as f, and one or more arguments passed as x values, answer the following:

Sample solution

**Unintended glitch in the question that was caught by at least one person:
  the 'list in the sample calls should have just been list, e.g. (reduceArgs list 3 4 5 6)

Other than the glitch in the call examples,
the macro does, in fact, work as intended.

The macro's base case, e.g. (reduceArgs list x y), would indeed
    generate (foo x y) since others would be null

The macro's general case takes the x1 and x2 two data values
   and transforms them into a single element, i.e. (f x1 x2),
   which will be the x1 in the recursive call.

   The current first element of others then becomes the x2 value
   for the recursive call, thus peeling off one more argument
   with each layer of recursion.

   The construction thus layers:
          (f x1 x2)
       (f (f x1 x2) x3)
    (f (f (f x1 x2) x3) x4)
    etc.

   Thus a call like (reduceArgs list 3 4 5 6) becomes
      (list (list (list 3 4) 5) 6)