CSC 530 Lecture Notes Week 8

CSC 330 Lecture Notes Week 8
Introduction to Functional Programming and Lisp




  1. Relevant reading -- Lisp Primer, other Lisp documentation (as necessary), book Chapter 8.

  2. Functional versus imperative programming languages.
    1. In lecture notes 1, we distinguished between the von Neumann class of programming languages versus the non-von Neumann class.
      1. Synonyms for "von Neumann" are "imperative" and "non-functional".
      2. Synonyms for "non-von Neumann" are "applicative" and "functional".
    2. The imperative languages include all those we've studied so far in CSC 330.
    3. The applicative languages include Lisp, ML, and many others.
    4. We will study Lisp as the representative applicative language.

  3. A thought experiment -- assignmentless programming.
    1. One of the most distinctive features of a purely functional language is its lack of assignment statements.
    2. So, take your favorite imperative programming language and throw out assignment statements.
      1. What kind of programming could you do?
      2. Would any non-trivial programming even be possible?
      3. In case you're thinking that call-by-reference parameters and/or pointers might help, these are thrown out as well, as are for-loops, while-loops, and all other iteration constructs.
    3. While a language completely devoid of any form of assignment is extreme, such a language represents the essence of applicative programming.
    4. A fundamental tenet of applicative programming is that data do not change.
      1. Rather, data are manipulated and constructed as they pass through functions.
      2. Data are never mutated once having been constructed and a single datum is never accessed by more than one function.
      3. Data values are like electrons flowing through hardware wires.
    5. In order for useful and efficient computation to be performed in an applicative language, it cannot be constructed simply by removing assignment statements from some imperative language.
      1. Rather, we must provide applicative language primitives that allow us to effectively program without assignment statements, and without other imperative features.
      2. Typically, imperative languages do not have the necessary applicative primitives to allow us to get by without assignment.

  4. The necessary evil of imperative constructs.
    1. Very few real programming languages are completely applicative (or completely imperative, for that matter).
    2. In the practical world, languages are primarily one category or the other.
      1. For example, almost all imperative languages have constructs to define and apply recursive functions.
      2. Similarly, most applicative languages have constructs for some form of assignment and/or data mutation.
    3. We will begin our study of applicative languages from a pure standpoint -- no assignment and no other applicative constructs.
    4. Subsequently, we will see how imperative features can fit into an applicative framework (but not without some penalty).

  5. Motivation for the study of Pure Lisp.
    1. Pure Lisp defines the most fundamental aspects of all programming languages in a very simple and elegant way.
    2. It is useful as a vehicle to introduce purely functional programming.
    3. It is also useful as a vehicle to describe the meaning of both applicative and imperative programming.
    4. Related to the previous point, Lisp is a good tool for the rapid prototyping of language translators, to investigate how new language features can be used to write actual programs.

  6. General features of Pure Lisp, compared to the Pascal/C/Java family of languages (PCJ).
    1. Lisp has syntax different from PCJ languages, but this difference is profoundly unimportant.
    2. Pure Lisp is an untyped language.
      1. Unlike most PCJ languages, the types of Lisp objects are not declared explicitly.
      2. For example, a single variable can be used to hold a numeric value at one point in a computation, and a list value at some other point in the same computation.
      3. This aspect of Lisp is other imperative languages, including Basic and Perl; i.e., untypedness is not a functional feature.
    3. Lisp is an expression language.
      1. It does not have declarations or statements in the conventional sense of PCJ languages.
      2. Every construct in the language returns a value.
    4. The overall style of Lisp programming is recursive, not iterative.
    5. Lisp is built on a few simple and orthogonal primitives.
      1. Its simplicity helps those who study programming languages focus on fundamental concepts rather than unimportant details.
      2. The orthogonality of its primitives means that each primitive provides a single form of functionality, and there is no unnecessary overlap with other primitives (cf. the orthogonal bases of a vector space).

  7. The basis of a functional language -- the function definition.
    1. Here's a simple example
      (defun APlusB (a b)
          (+ a b)
      )
      
    2. For a conventional frame of reference, here's the equivalent definition in CJ:
      int APlusB(int a,b) {
          return a + b;
      }
      
    3. There are a number of observations we can make from this simple example.
      1. The basic concept of function definition is the same in Lisp as in an imperative language, including all of the in the PCJ family.
        1. A function has a declared name (e.g., APlusB).
        2. There are formal parameters (e.g., a and b).
        3. There is a computational body (e.g., (+ a b)).
      2. Note again that Lisp is an untyped language
        1. Neither the return value nor formal parameters for the APlusB function have declared types.
        2. Evidently, the parameters must be numeric (or a least addable), since the body of the function adds them together.
        3. However, the Lisp interpreter does not enforce any static type requirements on the formal parameters; any addition errors will be caught at runtime.
        4. It should be noted that Lisp's untypedness is not a necessary feature of an applicative language; for example, ML is both strongly typed and applicative.
      3. In Lisp's syntax, all expressions are given in prefix notation.
        1. In most modern programming languages, built-in functions (such as arithmetic) can be invoked using infix notation (e.g., "a+b") instead of prefix notation (e.g., "+(a,b)").
        2. In Lisp, all functions, whether built-in or user-defined, are invoked in prefix notation.
        3. Notice further the rather quirky function invocation syntax.
          1. Viz., the name of a function appears inside the parens, rather than outside, and the actual function parameters are separated by spaces rather than the more typical commas.
          2. Here's a side-by-side look at Lisp calling forms and the equivalent C forms:

            Lisp Form PCJ Form Remarks
            (+ a b) a + b Call the built-in addition function
            (APlusB 10 20) APlusB(10, 20) Call the user-defined function APlusB

        4. It should be noted that Lisp's unusual invocation syntax has nothing whatsoever to do with applicativeness or imperativeness, though there is a good reason for it (and it's a reason you'll certainly appreciate in Assignment 6).
      4. Notice the lack of a return statement in the Lisp function definition.
        1. This owes to Lisp being an expression language -- i.e., every construct returns a value.
        2. In the case of a function definition, the value that the function returns is whatever value its expression body returns.
        3. No explicit "return" is necessary (this takes some getting used to).
        4. This notion of no explicit returns is a fundamental feature of applicative languages, not just another Lisp quirk.

  8. The one and only applicative control construct -- cond (a.k.a., if-then-else)
    1. Lisp has a conditional control construct comparable to the if-then-elsif-else in imperative languages.
    2. The general form is the following:
      (cond  ( (test-expression1) (expression11) ... (expression1j) ) )
                    . . .
                 ( (test-expressionn) (expressionn1) ... (expressionnk) )
      
    3. The test-expressions are evaluated in order until the first true case is encountered (say the ith), whereupon the ith expression sequence is executed and the cond is finished; if no test expression is true, no expression sequence is executed.
    4. This conditional form takes some getting used to, but once mastered it is quite handy.
    5. Examples of its use are coming up.

  9. A suitable data structure for an applicative language -- the heterogeneous list.
    1. A Lisp list is a collection or zero more elements of any type.
      1. Since Lisp is an untyped language, list elements can be any type of data, including (recursively) lists themselves.
      2. Lists are a superset of arrays, and can easily be used to represent records.
      3. Further, the recursive nature of lists allows them to represent data structures that require pointers in imperative languages.
      4. Hence, the list is a single data structure that can easily represent most of the typical built-in data structures of imperative languages, as well as higher level structures defined in library packages such as Java's java.util.
    2. The precise definition of a Lisp list is as follows:
      1. A list is a collection of zero or more elements, enclosed in matching parentheses.
      2. List elements can be atoms or lists.
      3. An atom is a number, string, boolean, or identifier.
    3. There are only three fundamental list operations, from which all others can be derived:

      Operation Meaning
      car return the first element of a list
      cdr return everything except the first element of a list
      cons construct a new list, given an atom and another list

    4. The following fundamental relationships exist between the three list primitives:
      1. (car (cons X Y)) = X
      2. (cdr (cons X Y)) = Y
    5. Examples to follow shortly.

  10. A completely Lisp-specific function -- quote
    1. If one carefully examines the Lisp syntax we have discussed thus far, there is an interesting potential problem.
    2. Viz. there is no syntactic distinction between a function invocation and a list datum.
      1. E.g, consider the following two function definitions:
        (defun f (x) ... )
        (defun a (x) ...)
        
      2. Given these definitions, what does the following Lisp form mean?
        (f (a b))
        
      3. Is it
        1. A call to function f, with the list argument (a b)?
        2. A call to function f, with an argument that is the result of a call to function a with argument b?
      4. The answer is (b).
      5. That is, the default meaning for a plain list form in Lisp is a function call.
      6. To obtain the alternate meaning (a) above, we must use the Lisp quote function (usually abbreviated as a single quote character) to indicate that we want to treat a list as a literal datum.
      7. I.e., the following form produces meaning (a) above:
        (f '(a b))
        

  11. Iteration through recursion
    1. In applicative languages, the iterative control constructs found in imperative languages are replaced by recursion.
      1. Hence, a purely applicative language has no while, for, repeat, or the like.
      2. In fact, a while loop is fundamentally unsuited for inclusion in an applicative language, for reasons that we'll discuss a little later (you might want to think about this now, however).
    2. As an example, consider the following equivalent Lisp and CJ programs that compute the average of a list (array) of integers:
      1. Lisp:
        (defun avg (l)
            (/ (sum l) (length l))
        )
        
        (defun sum (l) (cond ((null l) 0) (t (+ (car l) (sum (cdr l)))) ) )
        (defun main () (avg '(1 2 3 4 5)) )
      2. CJ:
        int avg(int l[], int length) {
            int i, sum;
            for (i=0, sum=0; i<length; i++)
                sum += l[i];
            return sum/length;
        }
        
        main() { int l[] = {1,2,3,4,5}; printf("%d\n", avg(l, 5)); }
    3. Here are some key observations about these two definitions of the averaging program:
      1. In the Lisp version of sum, a technique commonly called tail recursion is used to effect iteration.
      2. To better understand how tail recursion is working, here is a transliteration of the Lisp sum function into C, assuming that normal Lisp built-in functions are still available (i.e., null, car, and cdr) and there is a built-in list datatype:
        int sum(list l) {
            if (null(l))          /* if the list l is empty */
                return 0;         /*   then return 0 as the summing result */
            else                  /* otherwise, */
                return car(l) +   /*   return the sum of the 1st list element plus */
                    sum(cdr(l);   /*     the sum of the rest of the elements */
        
      3. The idea of this tail recursion is that each recursive invocation of a function handles the first element of a list, and passes on the rest of the list (i.e., the tail) to another recursive invocation of the same function.
      4. The recursion stops when the list has been exhausted to null, by the tail having been reduced one element at a time.

  12. Another illustrative list-processing example.
    1. In all real Lisp environments, there are many more built-in list processing functions than just the three primitives.
    2. However, a fundamentally important feature of Lisp is that any list operation can be (easily) built using the three primitives.
    3. As an example, here is the implementation 1 of the built-in function nth, that returns the nth element of a list (starting from 0), if it exists.
      (defun my-nth (n l)
          (cond ( (< n 0) nil )
                ( (eq n 0) (car l) )
                ( t (my-nth (- n 1) (cdr l)) )
          )
      )
      
    4. This function uses a similar form of tail recursion as used in the sum function discussed above.
      1. The reader is invited to perform a careful dissection of my-nth.
      2. The dissection should help reveal precisely how the recursion is working.
      3. Such use of recursion is fundamental to successful functional programming.




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

Footnotes:

1 It's named "my-nth" since "nth" is built-in to Lisp, and we don't want to change its definition. Programmers can redefine any built-in Lisp function, with no interference whatsoever from the Lisp interpreter.