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 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.
+ | Simplifies the writing of the function. |
- | Difficult to prove correctness. |
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:
plist is an array of records (objects) of two fields: property and value.
valueType getprop( Property plist[], propType prop ) { int i = 0; while ( plist[i].property != prop && i < plist.length ) i++; if ( i < plist.length ) return plist[i].value; else return notFoundValue; }
plist is a property list of the form
(prop1 value1 prop2 value2 . . . propn valuen) ( defun getprop ( plist prop ) ( cond (( null plist ) nil ) (( eql ( car plist ) prop ) ( cadr plist )) ( t ( getprop ( cddr plist ) prop )) ) )
Nested recursion can be used in place of nested iteration.
E.g., Cartesian product of two lists:
First write a "distribute-left" function:
( defun distl ( m N ) ( cond ( ( null N ) nil ) ( t ( cons ( list m ( car N )) ( distl m ( cdr N )))) ) )E.g.,
( defun cartesian ( M N ) ( cond ( ( null M ) nil ) ( t ( append ( distl ( car M ) N ) ( cartesian ( cdr M ) N ) ) ) ) )
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:
- they are both atoms and are eq; or
- their cars are the same (i.e., equal) and their cdrs are the same.
( defun equal ( x y ) ( cond ( ( or ( atom x ) ( atom y )) ( eq x y )) ( ( equal ( car x ) ( car y )) ( equal ( cdr x ) ( cdr y )) ) ) )
( defun equal ( x y ) ( or ( and ( atom x ) ( atom y ) ( eq x y )) ( and ( not ( atom x )) ( not ( atom y )) ( equal ( car x ) ( car y )) ( equal ( cdr x ) ( cdr y )) ) ) )
There are three general classes of operations commonly performed on lists:
E.g., add the elements of a simple list:
( defun plusReduction ( aList ) ( cond ( ( null aList ) 0 ) ( t ( + ( car aList ) ( plusReduction ( cdr aList )))) ) )
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 ) ))
E.g., increment all the elements of a simple list:
( defun incrementMapping ( aList ) ( cond ( ( null aList ) nil ) ( t ( cons ( 1+ ( car aList )) ( incrementMapping ( cdr aList )) )) ) )
The unary function -- 1+ -- is written into this special-purpose function.
E.g., extract all the negative elements from a list of numbers:
( defun minusFilter ( aList ) ( cond ( ( null aList ) nil ) ( ( minusp ( car aList )) ( cons ( car aList ) ( minusFilter ( cdr aList ))) ) ( t ( minusFilter ( cdr aList ))) ) )
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 L_{1} ( f L_{2} . . . ( f L_{n} s ) . . . ))
E.g.,
( mapcar f L ) means (( f L_{1} ) ( f L_{2} ) . . . ( f L_{n} ))
E.g.,
( filter p L ) is a list consisting of all elements of L such that (p L_{i}) is true.
E.g.,
The functional argument to reduce, mapcar or filter can be specified by giving:
E.g., define function distl such that
( distl x N ) ==> (( x N_{1} ) ( x N_{2} ) . . . ( x N_{n} ))
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 f_{0} () ( list x y )) ( defun f_{1} ( x ) ( list x y )) ( defun f_{2} ( y ) ( list x y )) ( defun f_{3} ( x y ) ( list x y )) ( defun f_{4} ( 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 ) )
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