Programmers are not to be measured by their ingenuity and their logic but by the completeness of their case analysis.

Alan Perlis

Let’s start with a simplified definition for etypecase-of (assuming type= from Part I). This looks very similar to ecase-of:

(defmacro etypecase-of (type expr &body clauses)
  (let ((actual-type `(or ,@(mapcar #'first clauses))))
    (unless (type= type actual-type)
      (warn "Type mismatch: ~a vs. ~a" type actual-type))
    `(etypecase ,expr
       ,@clauses)))

In fact ecase-of is, conceptually, a special case of etypecase-of. You could define ecase-of in terms of etypecase-of:

(defmacro ecase-of (expr type &body clauses)
  `(etypecase-of ,expr ,type
     ,@(loop for (key . body) in clauses
             collect `((eql ,key) ,@body))))

Although this would probably compile to less efficient code.

How can you use this? Here’s a simple example (from Overlord). We use deftype to define a type as a disjunction:

(deftype target-timestamp ()
  "Possible formats for the timestamp of a target."
  '(or timestamp
    universal-time
    never
    far-future))

This defines a target timestamp as always being one of:

  • A local-time timestamp.
  • A universal time.
  • The unit type (singleton) never.
  • The unit type far-future.

It then becomes possible to define functions for handling timestamps that are statically checked to make sure they handle all the possible representations:

(defun round-down-to-nearest-second (ts)
  (etypecase-of target-timestamp ts
    ((or never far-future universal-time) ts)
    (timestamp
     (adjust-timestamp ts
       (set :nsec 0)))))

(Timestamps will be revisited when we discuss dispatch-case.)

This approach is useful any time you have multiple representations of the same thing.

There is a pattern in Common Lisp where functions on a type accept generalized “designators” of that type. Functions on packages accept package designators (packages, but also a string or symbol naming the package) and functions on pathnames accept both pathnames and strings. If you explicitly define your own designator types, you can use etypecase-of to make sure they are handled completely:

(deftype key-designator ()
  '(or null function-name function))
(defun canonicalize-key (k)
  (etypecase-of key-designator k
    (null #'identity)
    (function-name (fdefinition k))
    (function k)))

One situation where etypecase-of is uniquely useful is in data structures where memory usage is a concern. Say you have a data structure that uses different kinds of node. Rather than defining class hierarchies or ADTs, you can choose the most compact representation possible for each kind of node, like a symbol or cons cell, and rely on etypecase-of to make sure you handle every representation.

One property of Common Lisp’s type system that is not always obvious to people new to Common Lisp is that types are not nullable unless specified as such. There is such a value as nil, but it has its own type (null) that is not a subtype of any other type except t (top). (But watch out: there is also a type named nil; this is Common Lisp’s bottom type. There is no value of type nil, not even nil.)

etypecase-of can be used when explicitly making types nullable. This is similar in practice to the uses of Option or Maybe in MLs.

(etypecase-of (or null string) s ...)

This particular example is unidiomatic; since nil is false, you might as well use if. But not every situation where a type is semantically nullable actually uses nil. Consider Postmodern. In Postmodern nil is reserved for the empty list, while SQL NULL is represented by the keyword :null. (Similar rules are used by JSON parsers.)

Start by defining a type that represents a value that could be NULL:

(deftype db-value (type)
  `(and atom (or ,type pomo:db-null)))

You can then use that type to make sure you remember to handle NULL in database queries:

(etypecase-of (db-value timestamp) next-update
  (timestamp next-update)
  (db-null nil))

Or even to make sure you remember to handle both NULL and Lisp nil:

(etypecase-of (or null (db-value string)) f
  (string (if (equal f "html") :html :xml))
  ((or null db-null) :xml))

Common Lisp provides a “numeric tower” of number types with implicit rules for conversion and contagion. Sometimes, however, you need to be explicit about handling certain kinds of numbers. You can use etypecase-of to make sure you don’t forget anything:

(etypecase-of (or real complex) z
    ((or zero (and negative integer))
     (error 'undefined-value
            :operation 'log-gamma
            :operands (list z)))
    (single-float
     (lgammaf z))
    (double-float
     (lgamma z))
    (real (log-gamma (coerce z 'double-float)))
    ;; For complex values, use the Lanczos approximation.
    (complex (lgammaz z)))

It is straightforward to define a tuple type for heterogeneous lists of fixed length.

(deftype tuple (&rest types)
  (reduce (lambda (type1 type2)
            `(cons ,type1 ,type2))
          types
          :initial-value nil
          :from-end t))

You can then describe heterogenous lists of fixed length to the type system:

(deftype var-alias () 'bindable-symbol)
(deftype function-alias () '(tuple 'function bindable-symbol))
(deftype macro-alias () '(tuple 'macro-function bindable-symbol))
(deftype import-alias () '(or var-alias function-alias macro-alias))

You can then use these for dispatch:

(make-keyword
 (etypecase-of import-alias alias
   (var-alias alias)
   (function-alias (second alias))
   (macro-alias (second alias))))

I would not generally recommend this approach for runtime code (use structs) but it can be useful when taming old-fashioned code that uses lists for everything, or at compile time for handling fragments of macros and macroexpansions. (The actual definition of tuple in Serapeum is mostly designed for the macro fragment use case, so it has special handling for keywords and quoted symbols.)

Common Lisp has a parametric type, (satisfies X), where X is the name of a predicate; anything that satisfies the predicate is considered to satisfy the type. This can be used by itself, but is most often used in conjunction with another type.

In Overlord there are deftypes for such types as relative-pathname, directory-pathname, and absolute-pathname:

(deftype absolute-pathname ()
  '(and pathname (satisfies absolute-pathname-p)))
(deftype relative-pathname ()
  '(and pathname (satisfies relative-pathname-p)))
(deftype directory-pathname ()
  '(and pathname (satisfies directory-pathname-p)))

These types can then be used with etypecase-of to make sure that pathnames are handled correctly:

(etypecase-of (or string directory-pathname relative-pathname) name
  (relative-pathname (directory-exists (ensure-absolute name)))
  (string (directory-exists (ensure-pathname name :want-pathname t)))
  (directory-pathname (make 'directory-exists :path name)))

This approach is useful when you need to make distinctions within a type – whether because the type is provided to you and you cannot subclass it, or because the distinctions are ones the type system cannot represent.

An afterthought about etypecase-of. It can be useful even if you plan to ultimately make your representations extensible. I sometimes use it as scaffolding but remove it later. That is, I might use it during development to pin down part of the design, but once the code has stabilized, I might replace it with, say, defgeneric or pattern matching.