A frequently heard wish is that Common Lisp had better support for static types. I am skeptical. I like the idea, but Lispers are far from pushing the boundaries of what the type system of Common Lisp already offers.
What does the type system of Common Lisp offer that Lispers don’t use? For me, the big one is exhaustiveness checking.
case. A common pattern in Common Lisp code is dispatching on keywords:
(case (car next-step) (:hop (perform-hop)) (:skip (perform-skip)) (:jomp (perform-jump)))
The problem with this approach is that there is nothing preventing you from misspelling a keyword. (Yes, this has happened to me. In production, of course.)
But this doesn’t have to happen! Common Lisp has a type system! Watch:
(deftype step () '(member :hop :skip :jump)) (defun type= (type1 type2 &optional env) (and (subtypep type1 type2 env) (subtypep type2 type1 env))) (defmacro ecase-of (expr type &body clauses) (let ((actual-type `(member ,@(mapcar #'first clauses)))) (unless (type= type actual-type) (warn "Type mismatch: ~a vs. ~a" type actual-type)) `(case ,expr ,@clauses)))
This defines a new macro,
ecase-of, that takes an extra argument — a type — and signals a warning, at compile time, if the type of the clauses is not equal to the supplied type.
(This is a simplified implementation, but the real thing is only slightly more complicated.)
We can rewrite the previous example to use
(ecase-of step (car next-step) (:hop (perform-hop)) (:skip (perform-skip)) (:jomp (perform-jump)))
And when we compile it, we get a warning:
Type mismatch: STEP vs. (MEMBER HOP SKIP JOMP)
Oh, right — it’s
Simple as this macro is, it fulfills the highest purpose of any abstraction: it eliminates an entire class of bugs. You cannot misspell a keyword; you cannot forget a keyword.
This is a useful macro in itself, but we can go much farther with this idea. In future installments: