I’m suspicious of layers of defensive programming. In my experience, they obscure the sources of errors. Worse, I find it too easy to muddy the essence of an algorithm with assertions and fail to address all cases.
Better, I think, to take the approach of contracts: make assertions and raise well-defined and descriptive errors at the surface of an API. Write and test internal API functions relying upon errors raised directly from underlying APIs.
I realised that I had violated this principle in MY-LAST.
(defun my-last (l) (check-type l list) (if (and (consp l) (consp (cdr l))) (my-last (cdr l)) l))
It works correctly. However, the use of CHECK-TYPE hides incomplete case-analysis. For example, after removing the call to CHECK-TYPE,
CL-USER> (my-last 42) => 42
Had I thought less about what MY-LAST shouldn’t do and more about what it should, I might have written the following:
(defun my-last-revised (l) (cond ((consp (cdr l)) (my-last-revised (cdr l))) ((consp l) l) (t nil)))
Here, CDR becomes the heart of the algorithm, signalling an error on anything but a list. Where input is a list, all cases return a list. Also, MY-LAST-REVISED eliminates a test, taking advantage of fact that CDR will return NIL for an empty list.
This all underscores a far more important point: the value of the ANSI Common Lisp specification. As Erik Naggum said:
certain languages support serious programmers, and others don’t. e.g., I don’t think it is at all possible to become a serious programmer using Visual Basic or Perl. if you think hard about what Perl code will do on the borders of the known input space, your head will explode. if you write Perl code to handle input problems gracefully, your programs will become gargantuan: the normal failure mode is to terminate with no idea how far into the process you got or how much of an incomplete task was actually performed. in my view, serious programmers don’t deal with tools that force them to hope everything works.