Skip to content

LISP - Structure Options

Lisp - Customizing Structures with defstruct Options

Section titled “Lisp - Customizing Structures with defstruct Options”

The defstruct macro is a powerful tool for creating custom data structures. Beyond defining simple slots, it offers a variety of options to control a structure’s behavior, such as how it’s created, printed, and identified.

By default, defstruct creates a constructor function named make-<struct-name>. The :constructor option lets you customize this.

  • (:constructor): Suppresses the creation of any constructor.
  • (:constructor make-my-point): Renames the constructor to make-my-point.
  • (:constructor make-point (x y)): Creates a constructor that accepts arguments positionally, not by keyword. This is useful for creating concise constructors for small, frequently used structs.
;;; Define a point structure with a custom positional constructor
(defstruct (point (:constructor make-point (x y)))
x
y)
;; Create an instance using the custom constructor
(let ((p1 (make-point 10 20)))
(format t "Created point with X=~d, Y=~d~%" (point-x p1) (point-y p1)))
Created point with X=10, Y=20

These options control the naming of the type-checking predicate and the structure copier function.

  • :predicate: The default predicate is <struct-name>-p. You can rename it with (:predicate is-a-point). Use (:predicate) to disable it.
  • :copier: The default copier is copy-<struct-name>. You can rename it with (:copier duplicate-point). Use (:copier) to disable it.
(defstruct (user (:predicate userp) (:copier nil)))
name
id)
(let ((u1 (make-user :name "Alice" :id 101)))
(format t "Is u1 a user? ~a~%" (userp u1))
;; (copy-user u1) would cause an error because the copier was disabled.

The :print-object option (a more modern and user-friendly alternative to :print-function) allows you to define a custom function for how instances of your structure are displayed by print, format, etc. This is invaluable for debugging and creating readable output.

;; First, define the function that will do the printing.
;; It must accept two arguments: the object and the output stream.
(defun print-custom-point (point stream)
(format stream "<Point (~a, ~a)>" (point-x point) (point-y point)))
;; Now, define the structure and link it to our print function.
(defstruct (point (:constructor make-point (x y))
(:print-object print-custom-point))
x
y)
(let ((p1 (make-point 10 20)))
;; Now `print` and `format` will use our custom function automatically.
(format t "The point is: ~a~%" p1))
The point is: <Point (10, 20)>

The :include option provides simple single inheritance, allowing one structure to inherit the slots of another. This is a great way to reduce code duplication.

;; Base structure
(defstruct shape
(color "black"))
;; Circle inherits from shape and adds a radius
(defstruct (circle (:include shape))
radius)
(let ((c1 (make-circle :radius 10 :color "red")))
;; Accessors from both structs work
(format t "Circle color: ~a, radius: ~a~%" (circle-color c1) (circle-radius c1))
(format t "The circle is: ~s~%" c1))
Circle color: red, radius: 10
The circle is: #S(CIRCLE :COLOR "red" :RADIUS 10)

By default, structures may be implemented as lists or in some implementation-specific way. The :type option lets you force the underlying representation to be a list or a vector. This can be useful for performance tuning or interfacing with other code.

;; Define a point backed by a vector
(defstruct (vec-point (:type vector))
x y)
(let ((vp1 (make-vec-point :x 5 :y 15)))
(format t "Type of vector-point: ~a~%" (type-of vp1)))

The output will be a vector type, which might look like this (exact output may vary by implementation):

Type of vector-point: (SIMPLE-VECTOR 2)

Combining these options allows you to create highly specialized and robust data types. For complex object-oriented programming with multiple inheritance and method dispatch, consider using the Common Lisp Object System (CLOS) with defclass, which defstruct builds upon.