19.7 Structures of Explicitly Specified Representational Type

Sometimes it is important to have explicit control over the representation of a structure. The :type option allows one to specify that a structure must be implemented in a particular way, using a list or a specific kind of vector, and to specify the exact allocation of structure slots to components of the representation. A structure may also be “unnamed” or “named,” according to whether the structure name is stored in (and thus recoverable from) the structure.

19.7.1 Unnamed Structures

Sometimes a particular data representation is imposed by external requirements, and yet it is desirable to document the data format as a defstruct-style structure. For example, consider expressions built up from numbers, symbols, and binary operations such as + and *. An operation might be represented as it is in Lisp, as a list of the operator and the two operands. This fact can be expressed succinctly with defstruct in this manner: е

(defstruct (binop (:type list))
  (operator ’? :type symbol)
  operand-1
  operand-2)

This will define a constructor function make-binop and three selector functions, namely binop-operator, binop-operand-1, and binop-operand-2. (It will not, however, define a predicate binop-p, for reasons explained below.)

The effect of make-binop is simply to construct a list of length 3:

(make-binop :operator ’+ :operand-1 ’x :operand-2 5)
    (+ x 5)

(make-binop :operand-2 4 :operator ’*)
    (* nil 4)

It is just like the function list except that it takes keyword arguments and performs slot defaulting appropriate to the binop conceptual data type. Similarly, the selector functions binop-operator, binop-operand-1, and binop-operand-2 are essentially equivalent to car, cadr, and caddr, respectively. (They might not be completely equivalent because, for example, an implementation would be justified in adding error-checking code to ensure that the argument to each selector function is a length-3 list.)

We speak of binop as being a “conceptual” data type because binop is not made a part of the Common Lisp type system. The predicate typep will not recognize binop as a type specifier, and type-of will return list when given a binop structure. Indeed, there is no way to distinguish a data structure constructed by make-binop from any other list that happens to have the correct structure.

There is not even any way to recover the structure name binop from a structure created by make-binop. This can be done, however, if the structure is “named.”

19.7.2 Named Structures

A “named” structure has the property that, given an instance of the structure, the structure name (that names the type) can be reliably recovered. For structures defined with no :type option, the structure name actually becomes part of the Common Lisp data-type system. The function type-of, when applied to such a structure, will return the structure name as the type of the object; the predicate typep will recognize the structure name as a valid type specifier.

For structures defined with a :type option, type-of will return a type specifier such as list or (vector t), depending on the type specified to the :type option. The structure name does not become a valid type specifier. However, if the :named option is also specified, then the first component of the structure (as created by a defstruct constructor function) will always contain the structure name. This allows the structure name to be recovered from an instance of the structure and allows a reasonable predicate for the conceptual type to be defined: the automatically defined name-p predicate for the structure operates by first checking that its argument is of the proper type (list, (vector t), or whatever) and then checking whether the first component contains the appropriate type name.

Consider the binop example shown above, modified only to include the :named option:

(defstruct (binop (:type list) :named)
  (operator ’? :type symbol)
  operand-1
  operand-2)

As before, this will define a constructor function make-binop and three selector functions binop-operator, binop-operand-1, and binop-operand-2. It will also define a predicate binop-p.

The effect of make-binop is now to construct a list of length 4:

(make-binop :operator ’+ :operand-1 ’x :operand-2 5)
    (binop + x 5)

(make-binop :operand-2 4 :operator ’*)
    (binop * nil 4)

The structure has the same layout as before except that the structure name binop is included as the first list element. The selector functions binop-operator, binop-operand-1, and binop-operand-2 are essentially equivalent to cadr, caddr, and cadddr, respectively. The predicate binop-p is more or less equivalent to the following definition.

(defun binop-p (x)
  (and (consp x) (eq (car x) ’binop)))

The name binop is still not a valid type specifier recognizable to typep, but at least there is a way of distinguishing binop structures from other similarly defined structures.

19.7.3 Other Aspects of Explicitly Specified Structures

The :initial-offset option allows one to specify that slots be allocated beginning at a representational element other than the first. For example, the form

(defstruct (binop (:type list) (:initial-offset 2))
  (operator ’? :type symbol)
  operand-1
  operand-2)

would result in the following behavior for make-binop:

(make-binop :operator ’+ :operand-1 ’x :operand-2 5)
    (nil nil + x 5)

(make-binop :operand-2 4 :operator ’*)
    (nil nil * nil 4)

The selectors binop-operator, binop-operand-1, and binop-operand-2 would be essentially equivalent to caddr, cadddr, and car of cddddr, respectively. Similarly, the form

(defstruct (binop (:type list) :named (:initial-offset 2))
  (operator ’? :type symbol)
  operand-1
  operand-2)

would result in the following behavior for make-binop:

(make-binop :operator ’+ :operand-1 ’x :operand-2 5)
    (nil nil binop + x 5)

(make-binop :operand-2 4 :operator ’*)
    (nil nil binop * nil 4)

If the :include is used with the :type option, then the effect is first to skip over as many representation elements as needed to represent the included structure, then to skip over any additional elements specified by the :initial-offset option, and then to begin allocation of elements from that point. For example:

(defstruct (binop (:type list) :named (:initial-offset 2))
  (operator ’? :type symbol)
  operand-1
  operand-2)

(defstruct (annotated-binop (:type list)
                            (:initial-offset 3)
                            (:include binop))
  commutative associative identity)

(make-annotated-binop :operator ’*
                      :operand-1 ’x
                      :operand-2 5
                      :commutative t
                      :associative t
                      :identity 1)
    (nil nil binop * x 5 nil nil nil t t 1)

The first two nil elements stem from the :initial-offset of 2 in the definition of binop. The next four elements contain the structure name and three slots for binop. The next three nil elements stem from the :initial-offset of 3 in the definition of annotated-binop. The last three list elements contain the additional slots for an annotated-binop.