As we continue through the less familiar parts of Lisp, next is the print function, and its relatives. This isn’t a trivial topic, so we’ll discuss it a bit. As I’m writing these articles at the newcomer to Lisp arriving from C++, let’s talk a bit about the C++ side of things.
In C, you had printf and write. The printf class of functions produces human-readable output in a printable representation, ASCII in the old days, but potentially wchar_t strings from extended character sets. With write, you could send values to disc in a compact binary format, not suitable for human reading, and not generally portable across platforms and implementations. The printf function could only act on language primitives, such as integers, floats, or pointers, and reading back with the scanf class of functions was brittle and computationally intensive. The write function could send entire structures or arrays to disc and re-read them, as long as you were very careful about the portability issues.
The C++ language, for reasons of parser compatibility, repurposed the bit-shift operators << and >> for input and output from streams. Now, it became possible to write customized output routines for classes and structures, and input routines that could re-read these from disc.
In Lisp, you can think of two different modes of output to character devices. There’s a human-readable format, and a machine-readable format. When we talk about writing something out “readably”, we mean in a machine-readable format, and more specifically, in a form that the Lisp reader, using the standard readtable, can parse to produce an object equivalent, by some definition, to the object that was printed.
You’ll note that in my examples I generally use the format function with the ~A control string. This instructs format to print the object presented, whatever it may be, for human reading (i.e. not “readably”). The equivalent form for readable output is the ~S control string.
So, what about Lisp print, and its relatives: write, prin1, pprint, and princ? Well, write is the low-level function that does the appropriate work. Its behaviour is controlled by a collection of keyword arguments and special variables (dynamically-scoped variables), but generally the programmer will be calling higher-level functions that will, themselves, invoke write.
We may not have discussed dynamically-scoped variables in much detail yet, so I’ll talk about that a bit here. A dynamically-scoped variable in Lisp looks quite unlike anything in C++. In its declaration, it looks a bit like a global variable, but is, in fact, very different. When a dynamically-scoped variable is reassigned in a binding, such as a let form, it appears as if it is pushing a new value onto a stack of values bound to the variable. Within the binding, in that thread of execution, the special variable has the most recently pushed value, and when flow exits the binding, the former value is restored. Here is an example:
(defparameter *my-special* 10)
(defun demonstrate ()
(labels
((print-my-special ()
(format t "*my-special* is ~A~%" *my-special*)))
(format t "On entry to demonstrate:~%")
(print-my-special)
(let ((*my-special* 20))
(format t "Inside the let:~%")
(print-my-special)
(setf *my-special* 30)
(format t "After the setf:~%")
(print-my-special))
(format t "After the close of the let:~%")
(print-my-special)))
with output:
CL-USER> (demonstrate)
On entry to demonstrate:
*my-special* is 10
Inside the let:
*my-special* is 20
After the setf:
*my-special* is 30
After the close of the let:
*my-special* is 10
NIL
So, with the use of special variables, it is possible for a Lisp thread of execution to alter certain parameters for printing without worrying about interfering with the behaviour of printing in other threads, and without having to worry about saving the former settings and restoring them on exit.
The print family of functions modify some of these special variables and call write. So, how do these functions differ?
First, prin1. It sets *print-escape* to non-nil, which means that the output is to be readable by the Lisp reader.
Next, princ, which sets *print-escape* and *print-readably* to nil. The *print-readably* variable implements a superset of *print-escape*. This function produces output intended to be read by people.
On to print, which behaves just like princ, but posts a newline before the object being printed, and a blank space after it.
Finally, pprint. This behaves like print, but does not append a trailing blank space. It also sets the *print-pretty* variable to non-nil, which tries to insert additional whitespace to improve the readability of the output. Here is an example from the CLHS. While the output here in SBCL does not match that in the CLHS, you can see that the variable has an effect:
CL-USER> (let ((*print-pretty* nil))
(progn (write '(let ((a 1) (b 2) (c 3)) (+ a b c))) nil))
(LET ((A 1) (B 2) (C 3)) (+ A B C))
NIL
CL-USER> (let ((*print-pretty* t))
(progn (write '(let ((a 1) (b 2) (c 3)) (+ a b c))) nil))
(LET ((A 1) (B 2) (C 3))
(+ A B C))
NIL
So, why do you need to know this? Well, sometimes you’ve seen me writing specialized print-object methods for classes. These methods should properly inspect the value of *print-readably* and *print-escape*, and take special action in those cases. Rather than producing an output suitable for a human, it should use something like the make-load-form and make-load-form-saving-slots functions we reviewed earlier so as to produce an output suitable for the desired context.