Chapter 6 - Deeper into Structures

Structures were introduced in Chapter 3. In this chapter, we'll look at the most useful optional features of structures.

Default values let you omit some initializers, sometimes

Normally, if you create a new structure without specifying a value for some slot, that slot will default to NIL.

? (defstruct foo-struct a b c)
FOO-STRUCT

? (let ((foo-1 (make-foo-struct :a 1 :b "two")))
    (print (foo-struct-b foo-1))
    (print (foo-struct-c foo-1))
    (values))
"two"
NIL
NOTE: We use the (values) form to suppress the return value from the LET form. Otherwise, we would have seen one more NIL printed.
In cases where NIL is a reasonable default value, this behavior is acceptable. But if the normal value of a slot is numeric, for example, you'd really like to start with a reasonable default value rather than having to add a test in all of the code which uses a structure. The full form of a slot specification is a list of the slot name, its default value, and additional options; specifying a bare name instead of the complete list is shorthand for "default value of NIL, and no options."

? (defstruct ship
    (name "unnamed")
    player
    (x-pos 0.0)
    (y-pos 0.0)
    (x-vel 0.0)
    (y-vel 0.0))
SHIP
When we instantiate this structure using (MAKE-SHIP), the NAME slot defaults to "unnamed", the PLAYER slot defaults to NIL, and the position and velocity slots all default to 0.0.

Of course, we can still specify slot values to override the defaults:

? (make-ship :name "Excalibur" :player "Dave" :x-pos 100.0 :y-pos 221.0)
#S(SHIP :NAME "Excalibur" :PLAYER "Dave" :X-POS 100.0 :Y-POS 221.0 :X-VEL 0.0 :Y-VEL 0.0)
Lisp's default printer for structures makes it easy to see the slots and their values. We've given explicit values to all of the slots except the two velocity slots, which have their default values.

Change the way Lisp prints your structures

To print a structure using other than the default printer, you may define a new print function as a structure option.

? (defstruct (ship
              (:print-function
               (lambda (struct stream depth)
                 (declare (ignore depth))
                 (format stream "[ship ~A of ~A at (~D, ~D) moving (~D, ~D)]"
                         (ship-name struct)
                         (ship-player struct)
                         (ship-x-pos struct)
                         (ship-y-pos struct)
                         (ship-x-vel struct)
                         (ship-y-vel struct)))))
    (name "unnamed")
    player
    (x-pos 0.0)
    (y-pos 0.0)
    (x-vel 0.0)
    (y-vel 0.0))
SHIP

? (make-ship :name "Proud Mary" :player 'CCR)
[ship Proud Mary of CCR at (0.0, 0.0) moving (0.0, 0.0)]
Actually, it's considered bad practice to print something the reader can't interpret. Our use of the brackets around the printed ship description is not necessarily good or bad, it depends upon how the current read table is specified (we first saw reader macros in Chapter 3, Lesson 12.

One way to ensure that the reader doesn't get confused is to deliberately print something so as to be unreadable. By convention, Lisp prints such objects beginning with #<. You could change your format string to read "#<ship ~A of ~A at (~D, ~D) moving (~D, ~D)>", so the prior MAKE-SHIP example would print #<ship Proud Mary of CCR at (0.0, 0.0) moving (0.0, 0.0)>. However, since 1990 Lisp systems have had a PRINT-UNREADABLE-OBJECT macro which should be used for this purpose. If the printer control variable *PRINT-READABLY* is true, PRINT-UNREADABLE-OBJECT will signal an error.

;; Use PRINT-UNREADABLE-OBJECT macro -- changes in boldface  
? (defstruct (ship
              (:print-function
               (lambda (struct stream depth)
                 (declare (ignore depth))
                 (print-unreadable-object (struct stream) 
                   (format stream "ship ~A of ~A at (~D, ~D) moving (~D, ~D)"
                           (ship-name struct)
                           (ship-player struct)
                           (ship-x-pos struct)
                           (ship-y-pos struct)
                           (ship-x-vel struct)
                           (ship-y-vel struct))))))
    (name "unnamed")
    player
    (x-pos 0.0)
    (y-pos 0.0)
    (x-vel 0.0)
    (y-vel 0.0))
SHIP

Alter the way structures are stored in memory

Lisp stores structures in an implementation-dependent manner unless you specify otherwise using a structure option. You have two choices if you decide to specify structure storage: store it as a vector (possibly with a particular type for all of the elements) or as a list. Here, we use the untyped vector option -- the list option is similar:

? (defstruct (bar
              (:type vector))
    a b c)
BAR

? (make-bar)
#(NIL NIL NIL)
Note that the slot names are not stored when you specify the storage type. This is probably the biggest advantage for using this option -- it can save storage in the amount of a machine word per slot per instance. The disadvantage is that Lisp does not recognize such a structure as a distinct type, and does not create a <structure-name>-P predicate for you.

If you are satisfied with being able to retrieve the name of the structure, but still want the storage savings associated with specifying the structure's representation, you can do this:

? (defstruct (bar
              (:type vector)
              :named)
    a b c)
BAR

? (make-bar)
#(BAR NIL NIL NIL)
Using the list representation option has the drawbacks noted above, but none of the advantages; the backbone of the list typically adds a machine word of storage per slot when compared to the default representation, which is usually a vector. The only time it would make sense to explicitly specify a list representation is when the default structure representation is list-based or when the Lisp implementation imposes some artificial limit on the space reserved for storage of vectors; neither case applies in modern implementations.

Shorten slot accessor names

Slot accessor names are constructed from the name of the structure and the slot. If the structure and the slot both have lengthy names, the accessor names can get unwieldy. You can abbreviate names somewhat by using the :CONC-NAME structure option to specify a name to use instead of the structure name.
? (defstruct (galaxy-class-cruiser-ship
              (:conc-name gcc-ship-)) ; name includes trailing hyphen! 
    name player (x-pos 0.0) (y-pos 0.0) (x-vel 0.0) (y-vel 0.0))
GALAXY-CLASS-CRUISER-SHIP

? (let ((ship (make-galaxy-class-cruiser-ship)))
    (print (gcc-ship-x-pos ship)) ; note abbreviated accessor name 
    (values))
0.0

Allocate new structures without using keyword arguments

For certain structures, it may be more convenient to make a new instance using just a list of arguments instead of keywords and arguments. You can redefine a structure constructor's argument list using the :CONSTRUCTOR option.

? (defstruct (3d-point
              (:constructor
               create-3d-point (x y z)))
    x y z)
3D-POINT

? (create-3d-point 1 -2 3)
#S(3D-POINT :X 1 :Y -2 :Z 3)
NOTE: The slot values do not default to NIL if you use a :CONSTRUCTOR option!
Most lambda-list options are available to the constructor function -- consult a Lisp reference manual for details.

Define one structure as an extension of another

We use inheritance to define one object in terms of another. Structures permit a very simple form of inheritance using the :INCLUDE option.

? (defstruct employee
    name department salary social-security-number telephone)
EMPLOYEE

? (make-employee)
#S(EMPLOYEE :NAME NIL :DEPARTMENT NIL :SALARY NIL :SOCIAL-SECURITY-NUMBER NIL :TELEPHONE NIL)

? (defstruct (manager
              (:include employee))
    bonus direct-reports)
MANAGER

? (make-manager)
#S(MANAGER :NAME NIL :DEPARTMENT NIL :SALARY NIL :SOCIAL-SECURITY-NUMBER NIL :TELEPHONE NIL :BONUS NIL :DIRECT-REPORTS NIL)
All accessors which apply to an EMPLOYEE also apply to a MANAGER, and a MANAGER instance is also an EMPLOYEE instance. Notice in the following example how the ...-NAME accessors for both MANAGER and EMPLOYEE reference the same slot.

? (setq mgr (make-manager))
#S(MANAGER :NAME NIL :DEPARTMENT NIL :SALARY NIL :SOCIAL-SECURITY-NUMBER NIL :TELEPHONE NIL :BONUS NIL :DIRECT-REPORTS NIL)

? (setf (manager-name mgr) "Buzz")
"Buzz"

? (employee-name mgr)
"Buzz"
A structure may have one :INCLUDE option, at most. This limits the ability of structures to model the real world by describing inheritance. CLOS objects allow multiple inheritance, and have many other useful and convenient features. We will get our first look at CLOS in Chapter 7.
Contents | Cover
Chapter 5 | Chapter 7
Copyright © 1995-1999, David B. Lamkins
All Rights Reserved Worldwide

This book may not be reproduced without the written consent of its author. Online distribution is restricted to the author's site.