Next, we look at the typecase macro. As Lisp does not enforce types of variables, it is valid for different code paths to set a particular variable or parameter to different data types. The programmer may want to write code to handle all the cases. Whereas in C++ one typically would overload a function based on its parameter types, with the compiler assigning the correct function at compile time, in Lisp one would branch code paths based on the run-time determined parameter type.
Now, in C++, it’s common for one function to be the actual worker, and the other functions to be thin wrappers around it. The non-worker functions simply convert the arguments into a form suitable for processing by the single worker function. This avoids unnecessary duplication of code and mysterious inconsistencies when one function is modified or debugged while leaving the other functions alone. In Lisp, the similar construct is to recurse into itself with the modified types. Here is an example:
typecase.lisp
(defstruct moments
(sum-x 0.0d0)
(sum-x2 0.0d0))
(defun add-values (moment-struct &rest to-add)
(dolist (one-to-add to-add)
(typecase one-to-add
(number
(incf (moments-sum-x moment-struct) one-to-add)
(incf (moments-sum-x2 moment-struct) (* one-to-add
one-to-add)))
(cons
(dolist (entry one-to-add)
(add-values moment-struct entry)))
(vector
(dotimes (i (length one-to-add))
(add-values moment-struct (aref one-to-add i))))))
moment-struct)
The output from a typical set of calls:
*slime-repl sbcl*
CL-USER> (add-values (make-moments) '((1 2) (3 (4 5)) 6))
#S(MOMENTS :SUM-X 21.0d0 :SUM-X2 91.0d0)
CL-USER> (add-values (make-moments) '((1 2) (3 (4 5)) 6 nil))
#S(MOMENTS :SUM-X 21.0d0 :SUM-X2 91.0d0)
CL-USER> (let ((ones-vec (make-array 5 :initial-element 1)))
(add-values (make-moments) ones-vec))
#S(MOMENTS :SUM-X 5.0d0 :SUM-X2 5.0d0)
You’ll note that the
add-values function I’ve written ignores any arguments that are not list, vector, or numeric. It may be desirable, instead, to use
ctypecase or
etypecase to signal correctable or non-correctable errors, respectively, when an unexpected type is passed. That is typically the way I code such constructs, I prefer programs to error out than to silently ignore data passed to them, but how one wants to do this will depend on context and the requirements of the code.
Finally, I’ll point out one other place where I’ve used typecase in the past, when writing macros. You may have seen an earlier series of posts that I put up on creating macros for a doubly-linked list structure related to my work. In my context, I needed an additional parameter on the looping macro, for the direction. The code had to behave slightly differently for forward and backward looping, specifically when building the increment function and loop exit condition. When the direction parameter was :FORWARD, some code was active, and when :REVERSE, other code was active. Often, that parameter was explicit in the code, sometimes it was the value of a symbol at runtime. When the compiler encounters a literal :FORWARD during macro expansion, it can determine that the reverse code path cannot be executed, and issues a warning about unreachable code. I don’t like my working code to generate warnings, they clutter the output and obscure problems. So, to avoid the warnings when literal keywords are used, I used typecase at macro expansion time. Here’s a piece of that code. You’ll note that the typecase is not in a backtick, so it is evaluated at macro expansion time. If the compiler determines that the direction parameter is a keyword, it inserts the appropriate single piece of code. If it is not a keyword, macro expansion falls through to the lower block which evaluates the direction parameter at runtime and then invokes the appropriate macro expansion.
typecase.lisp
(defmacro iter-loop-open-interval ((dll iter start end
&key (dirxn :FORWARD))
&body body)
"Loop through the dll while 'iter' ranges from from start+1 to end-1."
(let (first increment last)
(typecase dirxn
(keyword
(ecase dirxn
(:FORWARD
(setf first `(dl-list:iter-dir ,dll
,start
:PLUS
:circular t))
(setf increment `(iter-dir ,dll
,iter
:PLUS
:circular t))
(setf last `(dl-list:iter-dir ,dll
,end
:MINUS
:circular t)))
(:REVERSE
(setf first `(dl-list:iter-dir ,dll
,start
:MINUS
:circular t))
(setf increment `(iter-dir ,dll
,iter
:MINUS
:circular t))
(setf last `(dl-list:iter-dir ,dll
,end
:PLUS
:circular t))))
`(unless (or (eq ,start ,end)
(eq ,start ,last))
(do ((,iter ,first ,increment))
((eq ,iter ,end))
,@body)))
(t
`(ecase ,dirxn
(:FORWARD
(iter-loop-open-interval (,dll ,iter ,start ,end
:dirxn :FORWARD)
,@body))
(:REVERSE
(iter-loop-open-interval (,dll ,iter ,start ,end
:dirxn :REVERSE)
,@body)))))))