Programming Languages (CSC-302 99S)


Notes on Exam 1: Language Design, Functional Programming, and Scheme

This page may be found online at http://www.math.grin.edu/~rebelsky/Courses/CS302/98S/Handouts/examsoln.01.html.

I did not use the same grading strategy for this exam that I typically use on homeworks. In particular, in this exam you started with 100 points and lost points for problems in your answers. When appropriate, I've indicated some typical errors that lost you points.

1. The Design of Scheme

Evaluate Scheme using the criteria of simplicity, extensibility, and safety/security, as well as two other criteria of your own choosing. Your evaluations should be sufficient to convince me that you understand (1) the meaning of each criterion; (2) Scheme; and (3) how the two relate. You should not restrict your discussion to what you've learned in class. Often, you will need to refer to the Scheme report for further information.

Make sure that you choose interesting criteria.

Each criterion was worth ten points.

A number of you seemed to have ignored the Scheme report, even though I noted in the question that ``you will need to refer to the Scheme report for further information''. In particular, a number of you ignored Scheme's built-in arrays (called ``vectors'' in Scheme, to the chagrin of a number of folks) as well as Scheme's macro capabilities. These were both particularly important in your discussions of extensibility.

I was surprised to see that a number of you tried to discuss implementations of Scheme (typically in an attempt to criticize the language). We haven't said much about the implementation (except that CPS is used to make it closer to assembly code), so it's not clear what you base your assumptions on.

A few of you gave the following critique: ``I can't read Scheme programs, so it's not a readable language''. Few novices can read all programs, so I didn't accept such critiques.

If you care, here are the additional criteria people chose (followed by the number of people who chose each criterion). Efficiency (1), Expressiveness (4), Machine Independence (4), Maintainability (1), Orthogonality (2), Preciseness (2), Readability (6), Restrictability (2), Storage Management (2), Uniformity (3), and Writability (4).

Oh, Scheme is a proper name, and hence should be capitalized.

Here are some short answers that I might have given. They are not intended to be exemplary or comprehensive.

Simplicity. Scheme is a deceptively simple language. On the surface, it seems simple. You have a very limited syntax and a restricted number of built-in functions. However, it is arguable that Scheme is far from simple. Many of the built-in functions are quite complex with difficult implications. As an example, consider call/cc. Scheme also provides many different ways to do the same or things. Consider the relationship between let, define, and lambda. For example, here are two potential definitions of filter using both let and lambda.

(define (filter1 pred lst)
  (if (null? lst) '()
      (let ((rest (filter pred (cdr lst))))
         (if (pred (car lst)) 
             (cons (car lst) rest)
             rest
         )
      )
  )
)
(define (filter1 pred lst)
  (if (null? lst) '()
      ((lambda (rest)
          (if (pred (car lst)) 
             (cons (car lst) rest)
             rest
          ))
       (filter pred (cdr lst))
      )
  )
)

(In fact, many people define let in terms of lambda.)

There are also many closely-related concepts with similar (or even the same) syntax. Consider the many variations of let, including standard let, named let, and letrec.

Extensibility Scheme is clearly extensible in the standard simple ways: you can add new functions through libraries. You can also build your own data types by using its core built-in data types (using cons cells for dynamic data structures and vectors for array-like things). However, there's no real type system underlying these new type definitions, so your mileage may vary. (You'll need to define a new type by deciding on an underlying representation and choosing a collection of methods for manipulating that representation.)

But Scheme has many features that make it seem even more extensible. For example, continuations provide one mechanism for adding new control structures (e.g., the break we did in class, coroutines, and even exceptions and exception handling).

As you know, Scheme supports both functional and imperative programming. One might even claim that it's possible (easy) to do object-oriented programming in Scheme, since an object is simply a collection of data and operations, and its easy to make lists of data and operations. Inheritance and polymorphism are harder.

Finally, Scheme provides for Macros (described in Section 4.3 of the Scheme report). Scheme has a very powerful macro facility that, in effect, lets you change the syntax.

As I mentioned in class, extensibility is more than just the ability to add new functions and macros. I looked for some discussion of types, control, and syntax.

Safety Scheme has features that make it safer, and features that make it quite unsafe. The inclusion of a system for garbage collection along with a simple, somewhat implicit, allocation system make it a safer language.

Unfortunately, all of Scheme's type-checking is done at run time, which makes it a somewhat less safe language; it's difficult to determine whether a program has logical errors (at least the kind of logical errors a type system would identify) without running the program. Related to the lack of a type system is a lack of requirements for declarations.

I looked for discussion of type system (with variable declarations) and memory allocation, both of which affect the programmer's ability to write correct programs.

Here are a few more that I thought were interesting.

Readability Scheme's relative simple syntax, its multiple mechanisms for doing the same thing, and its support for many different programming styles all decrease its readability. While those new to Scheme can quickly learn to read simple Scheme programs, many find it difficult to understand more significant programs that employ a variety of advanced techniques. And, since different programmers use different techniques, it is often difficult for one programmer to understand another's program.

Uniformity and Orthogonality Are similar things expressed similarly and different things differently? Function application is always expressed in the same way, using prefix notation, unlike languages such as Pascal that sometimes use prefix notation (for functions) and sometimes use infix notation (for ``operators'', a particular class of built-in functions). Any function can be applied to any argument, which makes the language seem more uniform. (Of course, you may get a run-time error, but that's another issue).

However, there are some things that severely impact Scheme's uniformity. One of the most important has to do with evaluation of arguments to functions. Consider

(list (cons a nil))

This says ``apply cons to a and nil, then apply the list operator to that result''. Compare that to the quite similar

(quote (cons a nil))

That says ``apply quote to the list (cons a nil) without evaluating the cons''. There are only a few functions that don't immediately evaluate their arguments (lambda and if are two others), but the difference in evaluation styles is clearly a violation of uniformity.

2. Quicksort

Write a tail-recursive version of Quicksort in Scheme using continuation-passing-style. You are not permitted to use an explicit stack. Your Quicksort function should take the comparison operation as a parameter.

You may recall writing Quicksort for assignment 2. Obviously, this is a slightly different version.

In case you've forgotten, the basic structure of Quicksort is

In the array-based version of Quicksort, you build the two sublists using a pivot function. In the list-based version, you may have to find a slightly different way to build the two sublists.

Basic grading criteria

Most of you got this correct (or sufficiently correct to satisfy me and to give you a good grade). Those who didn't either forgot to take the comparison function as a parameter or didn't completely get CPS.

The following code has not yet been tested. Sorry.

We'll begin with the helper function split. This is a variant of split that splits the list into three sublists, rather than two. These are the elements smaller than the pivot, the elements equal to the pivot, and the elements greater than the pivot. This variant makes it possible to use pivots not in the list. The pivot function is naturally written in continuation-passing style: since we'd like to return three lists, we'll instead accept a 3-ary continuation.

;;; Given a list, a pivot, a comparison function, and a 3-ary continuation,
;;; splits the list into three parts: a list of elements smaller than the
;;; pivot, a list of elements equal to the pivot, and a list of elements
;;; greater than the pivot (or, more precisely, elements that the pivot is
;;; less than).  Then calls the continuation on those three parts (in order).
(define (split-cps lst pivot lessThan cont)
  (cond (
    ; Base case: the base list is empty.  The three sublists are therefore
    ; empty, so we can call the continuation on three empty lists.
    ((null? lst) (cont '() '() '()))
    ; Recursive case 1: the first element is less than the pivot.  Add
    ; to the front of the smaller list and continue.
    ((lessThan (car lst) pivot) 
       (split-cps (cdr lst) pivot lessThan (lambda (smaller same greater)
         (cont (cons (car lst) smaller) same greater))))
    ; Recursive case 2: the pivot is less than the first element.  Add to the
    ; front of the greater list and continue.
    ((lessThan pivot (car lst)) 
       (split-cps (cdr lst) pivot lessThan (lambda (smaller same greater)
         (cont smaller same (cons (car lst) greater)))))
    ; Recursive case 3: the pivot and the first element must be the same.  
    ; Add to the same list and continue.
    (default
       (split-cps (cdr lst) pivot lessThan (lambda (smaller same greater)
         (cont smaller (cons (car lst) same)  greater))))
  )) ; end cond
) ; end define

Here are some simple tests. I wasn't looking for particularly sophisticated testing, and I've probably done somewhat more than you did (especially since you weren't required to tet your helper functions). Here are some helper functions I've used in my testing.

;;; Split a list of numbers using split-cps and report on the results
(define (splitinfo pivot lst)
  (split-cps pivot lst (lambda (x y) (< x y)) (lambda (smaller same greater)
    (begin
      (display "Splitting ") (display lst) (newline)
      (display "Pivot ") (display pivot) (newline)
      (display "Smaller ") (display smaller) (newline)
      (display "Equal ") (display same) (newline)
      (display "Greater ") (display greater) (newline)
    ) ; begin block
  )) ; split
) ; define splitinfo

;;; Build a list of n even numbers starting with x.  Not in continuation-passing
;;; style.  x must be even.
(define (evens n x)
  (if (equal? 0 n) '()
      (cons x (evens (- n 1) (+ x 2)))))
      
;;; Join n copies of a list.  Not tail-recursive.
(define (ncopies n lst)
  (if (equal? n 0) '()
      (append lst (ncopies (- n 1) lst))))
      
;;; Test split-cps using a simple list and lots of interesting pivots.  We
;;; choose pivots that are in the list and pivots not in the list.  We'll
;;; us a lst of even numbers between 2 and 8 and try pivots between 1 and 9.
;;; The disadvantage of this strategy is that we need to read the output
(define (test-split-cps)
  (let ((lst (ncopies 5 (evens 2 4))))
    (letrec ((test-split-helper n)
      (if (< n 9)
        (begin
          (splitinfo n lst)
          (newline)
          (test-split-helper (+ n 1)))))
      (test-split-helper 1))))

Okay, we can now test. I'll only list some of the output.

> (test-split-cps)
...

We're now ready to move on to quicksort. We'll begin with a non-tail-recursive version.

;;; Sort a list using a specified comparison function.  Uses the well-known 
;;; quicksort function.
(define (quicksort lessThan lst)
  ; Base case: Empty list
  (if (null? list) '()
  ; Recursive case
    ; Pick a pivot
    (let ((pivot (car lst)))
    ; Split the list into three parts
    (split-cps lst pivot lessThan (lambda (smaller same greater)
    ; Sort the appropriate sublists and then join
    (append (quicksort lessThan smaller) same (quicksort lessThan greater)))))))

We developed a specified procedure for converting functions to continuation-passing style, so we can just use that algorithm.

Step 1. We build qs-cps by just copying the body and making a few minor changes so that we apply the continuation after computing each result.

;;; Sort a list using a specified comparison function.  Apply a function
;;; to the sorted list.  Uses the well-known quicksort algorithm.
(define (qs-cps lessThan lst cont)
  ; Base case: Empty list
  (if (null? list) (cont '())
  ; Recursive case
    ; Pick a pivot
    (let ((pivot (car lst)))
    ; Split the list into three parts
    (split-cps lst pivot lessThan (lambda (smaller same greater)
    ; Sort the appropriate sublists and then join
    (cont (append (quicksort lessThan smaller) 
                  same 
                  (quicksort lessThan greater))))))))

Step 2. We identify the calls to the original (pre-cps) function. There are two of them: one to sort the list of smaller elements and one to sort the list of greater elements. I'll call them S and G.

Step 3a. Identify the continuations of S and G. S's continuation is simply ``do G and then do the stuff after G''. G's continuation is ``append S, same, and G and apply the continuation to the result''.

The continuation for G is therefore

(lambda (G) (cont (append S same G)))

The continuation for S is then

(lambda (S) (continuation-for-G code-for-G))

Step 3b. Update to use the continuations.

;;; Sort a list using a specified comparison function.  Apply a function
;;; to the sorted list.  Uses the well-known quicksort algorithm.
(define (qs-cps lessThan lst cont)
  ; Base case: Empty list
  (if (null? list) (cont '())
  ; Recursive case
    ; Pick a pivot
    (let ((pivot (car lst)))
    ; Split the list into three parts
    (split-cps lst pivot lessThan (lambda (smaller same greater)
    ; Sort the appropriate sublists and then join
    (qs-cps lessThan smaller (lambda (S)
    (qs-cps lessThan greater (lambda (G)
    (cont (append S same G)))))))))))

Is this tail-recursive? Yes. If you look at each call to qs-cps, nothing else is done afterwards. The recursive calls do some things after computing the sorted list, but that's within their bodies, so that's okay.


Disclaimer Often, these pages were created ``on the fly'' with little, if any, proofreading. Any or all of the information on the pages may be incorrect. Please contact me if you notice errors.

This page may be found at http://www.math.grin.edu/~rebelsky/Courses/CS302/99S/Handouts/examsoln.01.html

Source text last modified Mon Feb 22 10:58:15 1999.

This page generated on Mon Feb 22 11:00:19 1999 by SiteWeaver. Validate this page's HTML.

Contact our webmaster at rebelsky@math.grin.edu