We’ve come to the end of pretty-printing, at least for the moment, and continue our walk through the less commonly used parts of Common Lisp with the print-object generic function. We’ve mentioned this function briefly in passing a few times already, a good example would be this early post.
The programmer must not call the print-object method directly, but that doesn’t mean it isn’t interesting to the programmer. The way the programmer typically interacts with this method is by specializing it on a new class or structure. The example post linked to above does that, for the doubly-linked list structures I was reviewing at the time.
The difficulty with the doubly-linked list structure was that, if you created one with two or more elements in it, and found yourself trying to display it to the user, the internal circularity of the structure led to an infinite loop during output, which was irritating during development. By specializing print-object on the structure, I was able to print out the contents of the list without the internal circularity being a problem.
The version of the function that I used there isn’t really complete, it’s just something short for debugging purposes. Properly, print-object should acknowledge the special variables that control printing, and format the output appropriately. The relevant variables are:
- *print-readably* which should produce output that would recreate the list if presented to the Lisp reader. Look at the earlier example for make-load-form.
- *print-escape* of which *print-readably* is a superset, should also produce output suitable for reading into the Lisp reader.
- *print-pretty* which should produce output intended to be more easily understood by humans, typically by the introduction of additional formatting whitespace.
- *print-length* which indicates how many elements in a sequence should be printed before the rest of the sequence is elided.
- *print-level* which is handled by the implementation if the print-object method obeys a simple restriction, that nested structures are handled by passing the sub-elements to write or similar output functions, not descended internally in the function itself. This allows the printer to keep track of the level of nesting that is being printed.
- *print-circle* which indicates that the calling context wants circularity detection to be performed, and if that requires special behaviour in the print-object method, it has to be coded to handle that case.
- *print-radix* and *print-base* which affect the printing of rational numbers. The programmer is unlikely to have to take special action on these variables, and can probably let them be passed down to the function responsible for printing the numeric values.
- *print-case* which affects how symbol names are output. Also unlikely to require special action by the programmer.
- *print-gensym* which affects the output of uninterned symbol names, such as those returned by gensym. Once more, the programmer is unlikely to have to write special code to respond to the value of this variable in print-object.
- *print-array* which determines whether non-string arrays should have their contents printed, or whether a short specifier should be output instead.
The print-object generic function should return a single value, the object passed in for printing. That is also something that was not correctly done in the earlier example.
Finally, now that we’ve covered *print-circle*, I should point out that that is another way to avoid the circularity problem printing out doubly-linked lists. Rather than defining a print-object method, I could have done my coding in an environment where *print-circle* is non-nil. In that case, the circularity detection allows the printer to avoid the infinite loop, and doubly-linked lists can be printed:
CL-USER> (let ((dl (dl-list:make-dl-list))) (dl-list:push-front dl 10) (dl-list:push-front dl 20) dl) #S(DL-LIST::DL-LIST :FIRST-NODE #1=#S(DL-LIST::DL-NODE :VALUE 20 :NEXT-NODE #2=#S(DL-LIST::DL-NODE :VALUE 10 :NEXT-NODE NIL :PREV-NODE #1#) :PREV-NODE NIL) :LAST-NODE #2#)