Defining Functions

Functions are defined in Lisp using the defun defining form. Defining a function with defun looks like this:

(defun fn-name (arg-1 ... arg-m) exp-1 ... exp-n) where fn-name is an identifier as are arg-1 to arg-m. The body of the function is the sequence of expressions exp-1 to exp-n. A function is called as was described earlier in function application, by using the identifier which names the function in the operator position of the application, for example: (fn-name exp-1 ... exp-m) calls the function defined with the name fn-name, passing as arguments the values of the expressions exp-1 to exp-m. Note that there are the same number of expressions in the call to the function as there are defined arguments in the definition. However, most Lisps offer a facility to define a function which accepts an arbitrary number of argument expressions. Beware! The syntax for this varies widely, but in EuLisp, it looks like this: (defun fn-name (arg-1 ... arg-m . arg-m+1) exp-1 exp-n) This defines a function which accepts at least m arguments. There are three cases to consider:
fewer than m arguments:
This is an error, and the system should tell you so, just as when the wrong number of arguments is supplied to a fixed arity function.
exactly m arguments:
arg-1 to arg-m receive the corresponding values of the argument expressions and arg-m+1 receives the value ().
more than m arguments:
arg-1 to arg-m receive the corresponding values of the argument expressions. arg-m+1 receives a list whose elements are the values of the remaining argument expressions.

Examples of function definition

Computing the length of a list

Since lists are a fundamental datatype in Lisp, it seems reasonable to start by defining a function which operates on lists, specifically, one to count the number of elements in a list. Of course, such a function is very likely already defined: in EuLisp this function is called size. We will define a function called list-length.

First, we must work out the algorithm. This also illustrates a common way of solving problems in Lisp and in the many functional languages descended from it and lambda-calculus (its theoretical underpinning), where we identify one case for which the result is trivial and the general case which is successively reduced to the trivial case. If you have done mathematical induction, you will find this obvious: this is just induction over a structure.

trivial case:
is of an empty list, that is (). The length of ()is 0.
general case:
is of a non-empty list. Given a list, we can obtain a list that is one element shorter by taking its cdr. Thus, the length of the non-empty list is 1 plus the length of its tail.
Consequently, we can make the following definition: (defun list-length (l) (if (null l) 0 (+ 1 (list-length (cdr l))))) Note the use of the predicate (returns true or false) function null. Its result is () if its argument is (), otherwise, it returns t.

To confirm our understanding of how list-length works, let us consider its application to the list '(a b c):

(length '(a b c)) => (+ 1 (length '(b c))) => (+ 1 (+ 1 (length '(c)))) => (+ 1 (+ 1 (+ 1 (length ())))) => (+ 1 (+ 1 (+ 1 0))) => (+ 1 (+ 1 1)) => (+ 1 2) => 3 where each of the (important) steps of the computation have been written out like substitutions into a formula.

Reversing a list

Another favorite beginning exercise in Lisp. To solve this problem we can use the append function. As before, we tackle the development of the algorithm by identifying a trivial case and a general case:
trivial case:
the empty list, the reverse of which is the empty list.
general case:
if we can reverse the tail of the list and then tack the head on to the end, we have reversed a non-empty list. The second bit uses append.
The code looks like this: (defun reverse-list (l) (if (null l) () (append (reverse-list (cdr l)) (list (car l))))) Note that append joins two lists, so we have to make the head of l into a list, using the constructor function list.

We can use the same analysis technique as before to see how reverse-list works:

(reverse-list '(a b c)) => (append (reverse-list (b c)) (list 'a)) => (append (append (reverse-list (c)) (list 'b)) (list 'a)) => (append (append (append (reverse-list ()) (list 'c))) (list 'b))) (list 'a)) => (append (append (append () (list 'c))) (list 'b))) (list 'a)) => (append (append (c) (list 'b))) (list 'a)) => (append (append (c b)) (list 'a)) => (c b a)

Raising an integer to an integer power

This example is instructive for two reasons: it can be used to illustrate the conversion of recursion to iteration (so called tail-recursion) and to illstrate a nice algorithmic improvement at the cost of a less transparent program.

We begin by noting the equation a^n = a^(n-1) * a and that a^0 = 1. From this, it is fairly straightforward to derive a program:

(defun pow (a n) (if (= n 0) a (* a (pow a (- n 1))))) If we use the method developed earlier to explain its execution, then we see the following: (pow 3 4) => (* 3 (pow 3 3)) => (* 3 (* 3 (pow 3 2))) => (* 3 (* 3 (* 3 (pow 3 1)))) => (* 3 (* 3 (* 3 (* 3 (pow 3 0))))) => (* 3 (* 3 (* 3 (* 3 1)))) => (* 3 (* 3 (* 3 3))) => (* 3 (* 3 9)) => (* 3 27) 81 This program has a sigificant inefficiency, in that the number of recursive calls is proportional to the argument n and hence, the depth of the stack is proportional to n. In algorithmic analysis terms, it is a linear or O(n) algorithm. In a procedural language such as FORTRAN, or Pascal, or C, although the program could be expressed in the same way, it would be more idiomatic to use a looping construct. Of course, it is well known that loops are a degenerate case of recursion, and one particular case of recursion can be optimized into a loop by the compiler: indeed, it is a feature programmers rely upon. This case is when the last function called in a function's body is itself. In fact, what happens is that the recursive call is replaced by a jump to the beginning of the function. We can rewrite pow to permit this optimization by adding a third parameter in which we accumulate the result: (defun pow (a n x) (if (= n 0) r (pow a (- n 1) (* a x)))) For this to work, the value of x in the initial call must be 1. Now the evaluation proceeds like this: (pow 3 4 1) => (pow 3 3 3) => (pow 3 2 9) => (pow 3 1 27) => (pow 3 0 81) 81 Now the number of recursive "jumps" is proportional to the argument n, as is the number of multiplications we carry out - as before - but the depth of the stack is fixed. We can reduce the number of recursive calls and the number of multiplications by noting the following equation: a^n = (a^(n/2))^2, n in 2Z. Hence, we can derive another program: (defun pow (a n) (if (= n 0) 1 (let ((r (pow a (/ n 2)))) (if (= 0 (% n 2)) (* r r) (* a r r))))) Abusing our now familiar visualisation technique: (pow 3 4) => ...(pow 3 2) => ... ... (pow 3 1) => ... ... ... (pow 3 0) => ... ... ... 1 => ... ... (* 3 1 1) => ... (* 3 3) => (* 9 9) 81 Although this is not markedly different from the earlier examples, the important point to note is that sometimes the intermediate result is squared. In this particular example, we have the same number of multiplications as in the previous version of pow, but consider the case of (pow 3 8). That can be achieved with one more multiplication here, while the previous version needs another four. In fact, the number of multiplications is proportional to the number of times the exponent can be divided by two, that is, its logarithm in base 2. Thus, this is called a logarithmic or O(log n) algorithm.

Reversing a list revisited

Now that we have been sensitized to the analysis of recursive algorithms and, in particular, the utility of conversion of proper recursion into tail recursion, we look again at the definition of reverse we gave earlier: (defun reverse-list (l) (if (null l) () (append (revers-list (cdr l)) (list (car l))))) The pattern of this function is identical to that of the power function immediately above. There is a recursive call with a reduced argument and the results of the recursive call are processed to build up the result. Thus, it should be obvious that reverse-list does O(n) append operations. However, let us consider a definition of append: (defun append (l1 l2) (if (null l1) l2 (cons (car l1) (append (cdr l1) l2)))) This. in turn, is linear in the cons operation, which is the most basic list allocation operator. Thus, reverse-list does O(n^2) cons operations to reverse a list of length n. A much more efficient solution is possible if we rewrite reverse-list to be tail recursive: the key idea is that as we take items from the front of the source list, they can be put on to the front of a result list. When the source list is exhausted, the result list will be the answer: (defun reverse-list (l x) (if (null l) x (reverse-list (cdr l) (cons (car l) x)))) This version is both tail recursive (the last function called is itself ) and uses a number of list cells proportional to the length of the original list, ie. it is O(n).

Defining local functions

While the use of an additional parameter has helped to improve the efficiency of the functions we considered above, there has been a price. The user of the function has to supply the initial value of that parameter, yet that value is private to the implementation of the function and is always the same. One solution is to define two functions, eg. (defun reverse-list (l) (reverse-list-aux l ())) (defun reverse-list-aux (l x) (if (null l) x (reverse-list-aux (cdr l) (cons (car l) x)))) However, a neater solution is to use a local function definition, by means of the labels form, which looks like this: (labels ((fn-name-1 arg-list-1 body-1) ... (fn-name-i arg-list-i body-i)) exp-1 ... exp-n) and in the case in question, allows us to make the following definition: (defun reverse-list (l) (labels ((loop (l x) (if (null l) x (loop (cdr l) (cons (car l) x))))) (loop l ())))

Anonymous functions

This may sound like a pointless idea: after all, if you can't name a function, you can't call it, nor can it be recursive. However, there are many situations when anonymous functions can be useful, although if you have not used a functional language before, you may not have felt their lack.

The notation for an anonymous function uses the special operator called lambda and looks like this:

(lambda (arg-1 ... arg-m) exp-1 ... exp-n) (lambda (arg-1 ... arg-m . arg-m+1) exp-1 ... exp-n) (Note: in Common Lisp, this is prefixed by #'.) This can be used in the function position in a function application: ((lambda (arg-1 ... arg-m) exp-1 ... exp-n) arg-exp-1 ... arg-exp-m) or as a functional argument in a function application: (f-name ... (lambda ...) ...) or, arguably more important, and certainly a new concept if coming from Pascal or C, as a functional result, returned from a function, for example: (defun add-n (n) (lambda (x) (+ x n))) (setq z (add-n 3)) => (lambda (x) (+ x 3)) (z 4) => (+ 4 3) 12 Here, we have defined a function called add-n, the result of which is an anonymous function which refers to the value of the argument passed in the call to add-n which caused the anonymous function to be created. Thus, the first application calls add-n with 3, returning an anonymous function. The second application calls the anonymous function, which is the value of z, resulting in the addition of 4 and 3.

You can also try a self-assessment test.


Julian Padget, jap@maths.bath.ac.uk, this version January 10, 1995