Sure, it’s nice to have exhaustiveness checking when dispatching on the type of a single object. But this is Lisp. What about multiple dispatch?

Lisp already has a syntax for multiple dispatch: generic functions. Could you layer exhaustiveness checking on top of generic functions? Of course. But while many Lisp types have corresponding built-in classes, being restricted to classes would still be limiting. Not all built in types have (portable) classes. And of course no satisfies types, no shortcuts with or, or so forth.

You can nest etypecase-of. But this is much harder to read and produces unhelpful errors and warnings. (What if more than one step has the same type?)

What dispatch-case provides is something that resembles defgeneric, but with exhaustiveness checking, and over any type.

Here is a simple example: using dispatch-case to compare different representations of timestamps.

(defun timestamp-newer? (ts1 ts2)
  "Is TS1 greater than TS2?"
  (dispatch-case ((ts1 target-timestamp)
                  (ts2 target-timestamp))
    ((target-timestamp never) t)
    ((target-timestamp far-future) nil)
    ((never target-timestamp) nil)
    ((far-future target-timestamp) t)
    ((timestamp timestamp)
     (timestamp> ts1 ts2))
    ((timestamp universal-time)
     (> (timestamp-to-universal ts1) ts2))
    ((universal-time universal-time)
     (> ts1
        ts2))
    ((universal-time timestamp)
     (> ts1 (timestamp-to-universal ts2)))))

But where dispatch-case shines is in complex problems.

One complex piece of code where dispatch-case was extremely helpful is the implementation of range in Serapeum.

The syntax of range is probably familiar from Python.

(range 10)
=> #(0 1 2 3 4 5 6 7 8 9)
(range 2 5)
=> #(2 3 4)
(range 0 10 2)
=> #(0 2 4 6 8)

A range function is not obviously useful in Lisp. Most of the time, you do not want to realize a sequence of numbers: both loop and Iterate have clauses to generate numbers as needed.

(A range function is not even obviously useful in Python; Python 2 had xrange to avoid it, and Python 3 range just returns an iterator.)

But sometimes you do want the actual sequence. Serapeum itself uses range to generate test data, as a replacement alexandria:iota, for a huge improvement in performance.

Why is it faster? Calling range doesn’t return a list of numbers; it returns a vector of numbers. But not just a vector; a vector specialized to the smallest type that can contain the numbers.

(range 256) returns an octet vector.

(type-of (range 256))
=> (SIMPLE-ARRAY (UNSIGNED-BYTE 8) (256))

(range 2) returns a bit vector (no, really):

(type-of (range 1))
=> (SIMPLE-BIT-VECTOR 1)

Exhaustiveness checking is helpful here because a thorough implementation of range needs to take great care to do the right thing when the arguments are mixed – mixed rationals and floats, or mixed single and double precision floats, or both. In particular, because of the rules of floating point contagion, no one argument by itself is enough to tell you the output type. And the output type has to be known in advance so a vector of the right kind can be allocated.

range is particularly fast on SBCL, because SBCL (to my pleasant surprise) does a fantastic job of compiling calls to range with constant arguments: it is able to recognize that if the arguments are constant, so is the element type of the resulting vector, and specializes the calls to aref.