Augustana University College

COMPUTING SCIENCE 370
Programming Languages


LISP -- Control Structures



LISP has two basic control structures:

Common LISP also provides a two-way conditional (if), a case structure, and various iterative control structures (for, do, when), and classical LISP included a primitive imperative control structure (prog), but they are not necessary for (or even antithetical to) functional programming and will not be further discussed.

The Conditional Expression

The conditional expression cond is one of the fundamental primitives of LISP.

   ( cond
      ( <test 1> <s-expr> <s-expr> . . . <s-expr> <result 1> )
      ( <test 2> <s-expr> <s-expr> . . . <s-expr> <result 2> )
         :
      ( <test n> <s-expr> <s-expr> . . . <s-expr> <result n> )
   )

Evaluation:

E.g., the signum (sign) function could be expressed as:

   ( cond
      (( minusp x ) -1 )
      (( zerop x )    0 )

      (( plusp x )    1 )
   )

However, since there are only three possible values that can be returned by this function (-1, 0, or 1), there is no point in performing the third test if the first two tests have each been false. To indicate the default or only other result, we can use t in place of the final test of the cond structure:

   ( cond
      (( minusp x ) -1 )
      (( zerop x)    0 )
      ( t            1 )
   )

If a clause has only one element and it evaluates to a non-nil value, then this value is returned as the value of the cond; e.g.,

   ( cond
      (( cdr aList ))
      ( t nil )
   )

LISP uses lazy evaluation (a.k.a. sequential, conditional, non-strict, or McCarthy evaluation):

   ( or x y )
is equivalent to
   ( cond
      ( x t )
      ( t y )
   )

Compare the Pascal condition

   if (aList = nil) or (head(aList) = 'key') then . . .
with the LISP condition
   (or (null aList) (eql (car aList) 'key))

The two are equivalent except that Pascal generates an error if aList = nil, since head(aList) would be evaluated even though it is undefined for an empty list. The LISP version avoids the run-time error because the second alternative is not evaluated if the first one is true.

Recursive Function Application

Recursive function application is the basis of most LISP functions/programs.

E.g.,

   ( defun reverse ( aList )
      ( cond
         (( null aList ) nil )
         ( t    ( append ( reverse ( cdr aList ))
                         ( list ( car aList ))
                )
         )
      )
   )

Recursion is theoretically equivalent to iteration, but is often easier to write.

E.g., iterative and recursive implementations of getprop:

Nested Recursion

Nested recursion can be used in place of nested iteration.

E.g., Cartesian product of two lists:

We can also recur (recurse?) on hierarchical structures, such as lists containing elements which are lists, just as the interpreter function eval does in evaluating arguments.

E.g., write a function equal which returns t if two s-expressions have the same structure. (Remember that eq and, except for numeric and character literals, eql check if their arguments point to the same atom or cons, i.e., have the same internal representation.)

Two s-expressions are equal if:
  1. they are both atoms and are eq; or
  2. their cars are the same (i.e., equal) and their cdrs are the same.

General Operations on Lists

There are three general classes of operations commonly performed on lists:

Reduction

E.g., add the elements of a simple list:

Note that the start value -- 0 -- and the binary function -- + -- are written into this special-purpose function.

Note also that the LISP function '+' is already defined as a reduction operator. However, rewriting the above function with '-' would give a different result than applying '-' to multiple arguments; to achieve the same result, reverse the order of the arguments to the binary function in the function above:

      ( t  ( - ( plusReduction ( cdr aList )) ( car aList ) ))
Mapping

E.g., increment all the elements of a simple list:

The unary function -- 1+ -- is written into this special-purpose function.

Filtering

E.g., extract all the negative elements from a list of numbers:

Abstracting the General Operations

The Abstraction Principle suggests that, instead of explicitly defining a different special-purpose function for each possible or desired reduction, mapping, and filter, we could pass a functional argument to one of the general functions reduce, mapcar, and filter.

Note: Only mapcar is provided as a standard function in classical LISP. A reduce function is available in Common LISP, but differs from the description below; the Common LISP function remove-if-not is like the reverse of filter.

( reduce f s L ) means ( f L1 ( f L2 . . . ( f Ln s ) . . . ))

E.g.,

( mapcar f L ) means (( f L1 ) ( f L2 ) . . . ( f Ln ))

E.g.,

( filter p L ) is a list consisting of all elements of L such that (p Li) is true.

E.g.,

Anonymous Functions (Lambda Expressions)

The functional argument to reduce, mapcar or filter can be specified by giving:

E.g., define function distl such that

   ( distl x N )  ==> (( x N1 ) ( x N2 ) . . . ( x Nn ))

We could first define a unary function pair:

   ( defun pair ( y )
      ( list x y )
   )

where x is fixed.

E.g.,

Then we could define distl:

   ( defun distl ( x N )
      ( mapcar 'pair N )
   )

E.g.,

IDEA: Why not put the definition of pair in the definition of distl?

   ( defun distl ( x N )
      ( mapcar '(list x y ) N )
   )

PROBLEM: We have not specified the parameters of the list function. Function mapcar requires a unary function (a function that accepts one parameter) as its first argument, but it is not clear how to interpret ( list x y ) as a unary function. It could be the body of any of the following functions:

   ( defun f0 ()      ( list x y ))
   ( defun f1 ( x )   ( list x y ))
   ( defun f2 ( y )   ( list x y ))
   ( defun f3 ( x y ) ( list x y ))
   ( defun f4 ( y x ) ( list x y ))

We need a notation to specify function definitions without naming them. LISP uses the notation:

E.g., ( lambda ( y ) ( list x y ))

Now distl can be written:

   ( defun distl ( x N )
      ( mapcar ( lambda ( y ) ( list x y )) N )
   )


What is "lambda"?
The lambda in a lambda expression is not an operator. It is just a symbol. In earlier dialects of Lisp it had a purpose: functions were represented internally as lists, and the only way to tell a function from an ordinary list was to check if the first element was the symbol lambda.
      In Common Lisp, you can express functions as lists, but they are represented internally as distinct function objects. So lambda is no longer really necessary . . . Common Lisp retained it for the sake of tradition.
Paul Graham, ANSI Common Lisp (Prentice Hall, 1996)


Functionals

Functional
A function that

Examples of functionals:

The Abstraction Principle suggests that we should define a general-purpose functional that converts a binary function to a unary one; i.e.,

such that

   ( defun bu (f x) 
      ( lambda (y) ( funcall f x y ))
   )

(funcall applies a function to the following arguments; see also apply.)

Now distl can be simplified:

   ( defun distl ( x n )
      ( mapcar ( bu 'list x ) n )
   )

This illustrates one of the benefits of abstraction: By using the bu functional, we avoid errors we might otherwise make in writing a lambda expression that converts a binary function to a unary function.

Copyright © 2000 Jonathan Mohr