Common Lisp is a language designed for development of large and complex applications. What in the 80s were thought to be large applications. But it got from production systems several facilities to deal with errors and even some support for compile-time checking. Still lots of code is written for prototypical software, research systems and/or personal purposes. You don't find always a high level of quality. Also keep in mind that sometimes very strict checking can make a code too strict (example: lots of HTTP clients will send non-conforming requests, but that's how it is and one can't easily reject them without losing a large number of possible users).
Let's look at some examples how Common Lisp helps you to write robust software:
strong typing and runtime type checking
We expect that a usual Lisp system will do runtime checks for each and every operation. Avoid Lisp systems which don't.
If you have a numeric function:
(defun foo (n x)
....
(bar ...))
(defun bar (a b)
(+ a b))
If FOO
does no argument checks, we expect that eventually the +
operation will check the arguments. At runtime there will be an error and an error handler will run, which, by default, will call a debugger.
Think about it: all (most) operations will be checked at runtime. All objects have a primitive type tag (integer, string, array, bit vector, character, stream, ...) and at runtime the type will eventually be checked.
But we expect more from the Lisp runtime:
- array bounds checks
- slot type checks
- heap consistency in case of errors
- various checks against harmful operations like redefining standard functions, deleting the Common Lisp package, arithmetic errors, etc.
Using a Lisp system which does not do runtime type checks is a huge pain. Now, Common Lisp allows us to declare parts of the code not to do runtime checks. Best strategy: find the smallest amount of code where it can be done without creating a risk (see LOCALLY
).
Argument lists
Common Lisp allows some argument list checking at compile time. Use it.
(defun foo (&key (n 1) (x 1.0))
...)
Now a typical compiler will catch a call like (foo :y 2 :x 2.0)
with an error: wrong keyword argument :y
.
Let the compiler check that the argument list has the right number of arguments and that the correct keyword arguments are being used.
CLOS, the Common Lisp Object System
Use CLOS.
(defmethod foo ((n integer) (x float)) ...)
If you define a method like above, at runtime in the method body n
will be an integer and x
will be a float. If you call FOO
with other argument types and no methods apply, then we get a runtime error.
Similar for instance slots: you can declare types.
(defclass bar ()
((x :type float)
(n :type integer)))
Use a Common Lisp implementation which actually checks those declarations or write your own checks.
Also: don't create raw data structures based on lists. Always package them into CLOS classes and methods. That way you get the right amount of runtime checking and introspection capabilities.
Check types at runtime
Common Lisp provides a macro for runtime type checking: CHECK-TYPE.
(defun foo (n x)
(check-type n integer)
(check-type x float)
(* (isqrt n) (sqrt x)))
The CHECK-TYPE
macro allows fancy type checking and even repairing the error.
CL-USER 27 > (foo 2000 5)
Error: The value 5 of X is not of type FLOAT.
1 (continue) Supply a new value of X.
2 (abort) Return to level 0.
3 Return to top loop level 0.
Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.
CL-USER 28 : 1 > :c 1
Enter a form to be evaluated: 5.0
Note that you can use the types to specify things like the interval for numbers, array dimensions or similar.
For example this checks that an object bound to the variable a1
is a two-dimensional array with dimensions 3 by 3:
(check-type a1 (array * (3 3)))
Note that you can define your own types with DEFTYPE
with arbitrary type predicates.
Use Lisp constructs which signal errors
For example ecase
vs. case
:
CL-USER 37 > (let ((code 10))
(ecase code
(1 'fine)))
Error: 10 fell through ECASE expression.
Wanted one of (1).
ecase
automatically signals an error, when no clause is matching.
The ASSERT
macro allows us to check arbitrary assertions.
Common Lisp provides a built-in ASSERT macro.
(defun foo (n x)
(assert (and (integerp n) (evenp n)) (n))
(assert (floatp x) (x))
(* (isqrt n) (sqrt x)))
Again, certain amount of runtime repair is available:
CL-USER 33 > (foo 2001 5.0)
Error: The assertion (AND (INTEGERP N) (EVENP N)) failed.
1 (continue) Retry assertion with new value for N.
2 (abort) Return to level 0.
3 Return to top loop level 0.
Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.
CL-USER 34 : 1 > :c 1
Enter a form to be evaluated:
2000
98.38699
Use CLOS for simple Design by Contract
(defclass bar ()
((n :type integer)
(x :type float)))
(defmethod setup-bar ((b bar) (n1 integer) (x1 float))
(with-slots (n x) b
(setf n n1 x x1))
b))
Now we can write an extra method to check for example that n
is larger than x
:
(defmethod setup-bar :before ((b bar) (n1 integer) (x1 float))
(assert (> n x) (n x)))
The :before method will always run before the primary method.
Add a Design by Contract system to CLOS
There are libraries for that. Quid Pro Quo is an example. There is also a simpler and older DBC implementation by Matthias Hölzl: Design by Contract.
Advanced error handling with the Condition System
Write condition types:
(define-condition mailer-incomplete-delivery-error
(mailer-error)
((recipient-and-status-list :initarg :recipient-and-status-list
:reader mailer-error-recipient-and-status-list)))
Above is a new condition, based on the mailer-error
condition. At runtime we can catch an SMTP response code and signal such a condition.
Write handlers and restarts to deal with errors. That's advanced. Extensive use of the condition system usually indicates better code.
Write and check tests
In many cases robust code needs a test suite. Common Lisp is no exception.
Let the user report errors
In many Common Lisp implementation one can get the error condition object, a backtrace and some environment data. Write those to an error log. Let the user report those. For example LispWorks has the :bug-form
command in the debugger.