The Common Lisp Cookbook – Type System

Table of Contents

The Common Lisp Cookbook – Type System

Common Lisp has a complete and flexible type system and corresponding tools to inspect, check and manipulate types.

Values Have Types, Not Variables

Being different from some languages such as C/C++, variables in Lisp are just placeholders for objects1. When you setf a variable, an object is “placed” in it. You can place another value to the same variable later, as you wish.

This implies a fact that in Common Lisp objects have types, while variables do not. This might be surprising at first if you come from a C/C++ background.

For example:

* (defvar *var* 1234)
*VAR*

* (type-of *var*)
(INTEGER 0 4611686018427387903)

The function type-of returns the type of the given object. The returned result is a type-specifier. In this case the first element is the type and the remaining part is extra information (lower and upper bound) of that type. You can safely ignore it for now. Also remember that integers in Lisp have no limit!

Now let’s try to setf the variable:

* (setf *var* "hello")
"hello"

* (type-of *var*)
(SIMPLE-ARRAY CHARACTER (5))

You see, type-of returns a different result: simple-array of length 5 with contents of type character. This is because *var* is evaluated to string "hello" and the function type-of actually returns the type of object "hello" instead of variable *var*.

Type Hierarchy

The inheritance relationship of Lisp types consists a type graph and the root of all types is T. For example:

* (describe 'integer)
COMMON-LISP:INTEGER
  [symbol]

INTEGER names the built-in-class #<BUILT-IN-CLASS COMMON-LISP:INTEGER>:
  Class precedence-list: INTEGER, RATIONAL, REAL, NUMBER, T
  Direct superclasses: RATIONAL
  Direct subclasses: FIXNUM, BIGNUM
  No direct slots.

INTEGER names a primitive type-specifier:
  Lambda-list: (&OPTIONAL (SB-KERNEL::LOW '*) (SB-KERNEL::HIGH '*))

The function describe shows that the symbol integer is a primitive type-specifier that has optional information lower bound and upper bound. Meanwhile, it is a built-in class. But why?

Most common Lisp types are implemented as CLOS classes. Some types are simply “wrappers” of other types. Each CLOS class maps to a corresponding type. In Lisp types are referred to indirectly by the use of type specifiers.

There are some differences between the function type-of and class-of. The function type-of returns the type of a given object in type specifier format while class-of returns the implementation details.

* (type-of 1234)
(INTEGER 0 4611686018427387903)

* (class-of 1234)
#<BUILT-IN-CLASS COMMON-LISP:FIXNUM>

Working with Types

The function typep can be used to check if the first argument is of the given type specified by the second argument.

* (typep 1234 'integer)
T

The function subtypep can be used to inspect if a type inherits from the another one. It returns 2 values:

For example:

* (subtypep 'integer 'number)
T
T

* (subtypep 'string 'number)
NIL
T

Sometimes you may want to perform different actions according to the type of an argument. The macro typecase is your friend:

* (defun plus1 (arg)
    (typecase arg
      (integer (+ arg 1))
      (string (concatenate 'string arg "1"))
      (t 'error)))
PLUS1

* (plus1 100)
101 (7 bits, #x65, #o145, #b1100101)

* (plus1 "hello")
"hello1"

* (plus1 'hello)
ERROR

Type Specifier

A type specifier is a form specifying a type. As mentioned above, returning value of the function type-of and the second argument of typep are both type specifiers.

As shown above, (type-of 1234) returns (INTEGER 0 4611686018427387903). This kind of type specifiers are called compound type specifier. It is a list whose head is a symbol indicating the type. The rest part of it is complementary information.

* (typep '#(1 2 3) '(vector number 3))
T

Here the complementary information of the type vector is its elements type and size respectively.

The rest part of a compound type specifier can be a *, which means “anything”. For example, the type specifier (vector number *) denotes a vector consisting of any number of numbers.

* (typep '#(1 2 3) '(vector number *))
T

The trailing parts can be omitted, the omitted elements are treated as *s:

* (typep '#(1 2 3) '(vector number))
T

* (typep '#(1 2 3) '(vector))
T

As you may have guessed, the type specifier above can be shortened as following:

* (typep '#(1 2 3) 'vector)
T

You may refer to the CLHS page for more information.

Defining New Type

You can use the macro deftype to define a new type-specifier.

Its argument list can be understood as a direct mapping to elements of rest part of a compound type specifier. They are be defined as optional to allow symbol type specifier.

Its body should be a macro checking whether given argument is of this type (see defmacro).

Now let us define a new data type. The data type should be a array with at most 10 elements. Also each element should be a number smaller than 10. See following code for an example:

* (defun small-number-array-p (thing)
    (and (arrayp thing)
      (<= (length thing) 10)
      (every #'numberp thing)
      (every (lambda (x) (< x 10)) thing)))

* (deftype small-number-array (&optional type)
    `(and (array ,type 1)
          (satisfies small-number-array-p)))

* (typep '#(1 2 3 4) '(small-number-array number))
T

* (typep '#(1 2 3 4) 'small-number-array)
T

* (typep '#(1 2 3 4 100) 'small-number-array)
NIL

* (small-number-array-p '#(1 2 3 4 5 6 7 8 9 0 1))
NIL

Type Checking

Common Lisp supports run-time type checking via the macro check-type. It accepts a place and a type specifier as arguments and signals an type-error if the contents of place are not of the given type.

* (defun plus1 (arg)
    (check-type arg number)
    (1+ arg))
PLUS1

* (plus1 1)
2 (2 bits, #x2, #o2, #b10)

* (plus1 "hello")
; Debugger entered on #<SIMPLE-TYPE-ERROR expected-type: NUMBER datum: "Hello">

The value of ARG is "Hello", which is not of type NUMBER.
   [Condition of type SIMPLE-TYPE-ERROR]
...

What about compile-time type checking?

Well, you may provide type information for variables, function arguments etc etc via the macro declare. However, similar to :type slot introduced in CLOS section, the effects of type declarations are undefined in Lisp standard and are implementation specific. So there is no guarantee that Lisp compiler will perform compile-time type checking.


  1. The term object here has nothing to do with Object-Oriented or so. It means “any Lisp datum”. 

Page source: type.md


© 2002–2019 the Common Lisp Cookbook Project
T
O
C