One of the reasons Lisp is still alive after so many years is its extensibility. As an example, Lisp can be easily extended with object oriented features, and Common Lisp defines a sophisticated package called the Common Lisp Object System or CLOS (pronounced "see-loss") for short, which offers everything you need to develop object oriented programs including some extensions not available in other OO languages.
Let's develop an account class similar to the one we have used as a Smalltalk example.
> (use-package "CLOS") T > (defclass account () ((balance :accessor balance :initform 0.0 :initarg :balance))) #<STANDARD-CLASS ACCOUNT>
Once we have imported the CLOS package (this step is required only for Clisp), the defclass call defines a class called account. The name of the class is followed by the list of superclasses which in our case is empty. Next is a list of attribute definitions. For now, our account has a single attribute balance. Attribute definitions are lists starting with the name of the attribute followed by a number of keyword arguments. We can define a default value for the attribute (:initform), whether it should be part of a constructor call (:initarg), and the accessor methods (reader/getter, writer/setter, or both) we would like to have. In our example, the attribute balance has the default value zero, can be set during the constructor call using the :balance keyword, and we would like a reader and a writer method, both called balance. Lisp's generic constructor function (comparable to new in other OO languages) is called make-instance.
> (setf a (make-instance 'account :balance 10.0)) #<ACCOUNT #x1A4EE1F1> > (balance a) 10.0 > (setf (balance a) 55) 55 > (balance a) 55
Note the call of the writer method using setf. The next thing we would like to do with the account is spending money.
> (defmethod spend ((a account) amount) (setf (balance a) (- (balance a) amount))) > (spend a 5) 50 > (balance a) 50
This definition looks more like a normal function definition rather than a method definition as we know it from the object oriented languages we have seen so far. The instance a does not play a prominent role. The argument list consists of simple arguments like amount and object arguments which are given as name and class. We could have as easily defined the method to take the amount as the first argument and the account object as the second.
> (defmethod spend2 (amount (a account)) (setf (balance a) (- (balance a) amount))) > (spend2 5 a) 45
We can also pass multiple objects.
> (defmethod sum ((a account) (b account)) (+ (balance a) (balance b))) > (sum a a) 90
To see the polymorphic behaviour of generic functions, we need a sub class of account.
(defclass checking-account (account) ((history :accessor history :initform nil)))
The checking-account class extends account with a history attribute which is initially set to nil. We will use this history attribute to keep track of the transactions on our account. To this end we need to change the implementation of the spend method.
(defmethod spend ((a checking-account) amount) (progn (setf (balance a) (- (balance a) amount)) (setf (history a) (cons '(spend amount) (history a)))))
In addition to updating the balance, we add the type and amount of the transaction to the history. Let's see how the new class behaves.
> (setf b (make-instance 'checking-account :balance 100.0)) #<CHECKING-ACCOUNT #x2041E6ED> > (balance b) 100.0 > (spend b 25.0) ((SPEND . 25.0)) > (spend b 10.0) ((SPEND . 10.0) (SPEND . 25.0)) > (balance b) 65.0 > (history b) ((SPEND . 10.0) (SPEND . 25.0))
As expected, the new implementation of the spend method applied to the checking account. Compared to other object-oriented languages, CLOS does not restrict this kind of polymorphism (dynamic dispatch) to a single argument. Generic functions can be specialized with respect to any of their arguments.
What we don't like about the implementation of the spend method for the checking account is the repetition of the code of the account's original implementation. Of course, Common Lisp has a way to achieve the same behavior without this code duplication. We can add behavior before or after an existing method by just placing the :before or :after keywords behind the method name. An equivalent better implementation is therefore
(defmethod spend :after ((a checking-account) amount) (setf (history a) (cons '(spend amount) (history a))))
The flexibility of Lisp comes from the possibility to extend Lisp using Lisp itself. Since programs and data use the same structure, we can use functions to create new programs. The key for doing this efficiently (at compile time) are macros. They are similar to C macros in that they generate code, but in the case of Common Lisp, macros have the full power of Lisp to construct this code. Here is a macro adding a while statements to Lisp.
> (defmacro while (test &rest body) `(do () ((not ,test)) ,@body)) WHILE > (setf i 0) 0 > (while (< i 5) (print i) (incf i)) 0 1 2 3 4 NIL
This example shows a number of typical macro features. First, the definition looks like the definition of a function apart from the use of defmacro instead of defun. The macro has a normal parameter list including a variable argument list body indicated by the &rest parameter. The body of the macro definition starts with a symbol we have not used before: the back quote. By iself, the back quote works just like the normal "forward" quote protecting its argument from evaluation.
> '(a (+ 4 5) b) (A (+ 4 5) B) > `(a (+ 4 5) b) (A (+ 4 5) B)
But in contrast to the normal quote, one can turn the evaluation on again for certain sub expressions by preceding those expression with a comma.
> `(a (+ 4 5) b) (A 9 B)
Finally, the odd operator comma-at ,@ expects a list argument and copies the elements of this list into the output.
> (setf x '(1 2 3)) (1 2 3) > `(a x b) (A X B) > `(a ,x b) (A (1 2 3) B) > `(a ,@x b) (A 1 2 3 B)
Combining all these features we can understand the while macro. A helpful tool for macro development is the macroexpand-1 function which shows the generated Lisp code.
> (macroexpand-1 '(while condition task)) (DO NIL ((NOT CONDIION)) TASK) > (macroexpand-1 '(while (< i 5) (print i) (incf i))) (DO NIL ((NOT (< I 5))) (PRINT I) (INCF I))
The macro construct a do loop using the back quote syntax and inserting the test condition with comma and the body of the while loop with the comma-at operator.
Exceptions as a means to organize error handling belong to the standard toolset of a modern programming language. Lisp talks about conditions rather than exceptions, but the idea is the same. You can raise a condition and thus interrupt the normal program flow, the condition can be caught using a handler expression (handler-case), and you can perform actions whether an exception is raised or not using ...
Common Lisp comes with a standard set of utilities which help to understand Lisp programs. The trace function allows to switch on tracing for a function. This feature is especially useful for recursive functions.
> (defun fac (n) (if (< n 2) 1 (* n (fac (- n 1))))) FAC > (trace fac) ;; Tracing function FAC. (FAC) > (fac 5) 1. Trace: (FAC '5) 2. Trace: (FAC '4) 3. Trace: (FAC '3) 4. Trace: (FAC '2) 5. Trace: (FAC '1) 5. Trace: FAC ==> 1 4. Trace: FAC ==> 2 3. Trace: FAC ==> 6 2. Trace: FAC ==> 24 1. Trace: FAC ==> 120 120
With the function untrace tracing is switched off again. The time function can be used for simple performance measurements (similar to the UNIX time command). This is particularly interesting when comparing the interpreted version of a function to its compiled form which is about eight times faster.
> (defun f (n) (dotimes (i n) nil)) F > (time (f 1000000)) Real time: 2.624 sec. Run time: 2.5837152 sec. Space: 0 Bytes NIL > (compile 'f) F ; NIL ; NIL > (time (f 1000000)) Real time: 0.34 sec. Run time: 0.3304752 sec. Space: 0 Bytes NIL