utilities.print-items
2022-11-07
A protocol for flexible and composable printing.
Upstream URL
Author
Maintainer
License
1Introduction
The utilities.print-items
system provides a protocol for flexible
and composable printing, primarily unreadable printing.
Why is this useful? Common Lisp has the generic function
cl:print-object
, which is often used to print compact and
intuitive yet unreadable (by cl:read
) representations of
objects. Thanks to CLOS, considerable flexibility and composability
would, in principle, be achievable. However, a common idiom is
(defmethod print-object ((object CLASS) stream)
(print-unreadable-object (stream :type t :id t)
CODE))
which prevents the flexibility provided by CLOS from actually being used:
- Calling the next method in a
print-object
method would eitherwrap the final result in multiple layers ofprint-unreadable-object
or some of the methods would not befully functional on their own. - Similarly,
:before
and:after
methods onprint-object
wouldproduce output parts outside of the prefix and suffix produced byprint-unreadable-object
.
2Tutorial
To illustrate the problem and the solution offered by the
utilities.print-items
system more concretely, consider the
following example:
(defclass name-mixin ()
((%name :initarg :name :reader name)))
(defclass value-mixin ()
((%value :initarg :value :reader value)))
(defclass binding (name-mixin value-mixin)
())
2.1The Problem
Due to the issues mentioned above, print-object
-based solutions
are not satisfactory:
(defmethod print-object ((object name-mixin) stream)
(format stream "~S" (name object))
(when (next-method-p)
(call-next-method)))
(defmethod print-object ((object value-mixin) stream)
(format stream "= ~S" (value object))
(when (next-method-p)
(call-next-method)))
(princ-to-string (make-instance 'binding :name "foo" :value 5))
"foo"= 5#<BINDING {100A912B83}>
This is somewhat composable and the output has all expected parts, but the arrangement of the output parts is completely wrong as well as hard to control.
(ignore-errors
(remove-method #'print-object (find-method #'print-object '() (list (find-class 'name-mixin) (find-class 't)))))
(ignore-errors
(remove-method #'print-object (find-method #'print-object '() (list (find-class 'value-mixin) (find-class 't)))))
The opposite approach would be:
(defmethod print-object ((object binding) stream)
(print-unreadable-object (object stream :type t :identity t)
(format stream "~S = ~S" (name object) (value object))))
(princ-to-string (make-instance 'binding :name "foo" :value 5))
#<BINDING "foo" = 5 {100BCD44B3}>
This produces the expected result but is not composable at all
since every user of name-mixin
and value-mixin
has to do all
the printing itself.
(ignore-errors
(remove-method #'print-object (find-method #'print-object '() (list (find-class binding) (find-class 't)))))
2.2The Solution
When using the utilities.print-items
system, print-object
methods are replaced by print-items:print-items
methods (note the
append
method combination) for mixin classes:
(defclass name-mixin ()
((%name :initarg :name :reader name)))
(defmethod print-items:print-items append ((object name-mixin))
`((:name "~S" ,(name object))))
(defclass value-mixin ()
((%value :initarg :value :reader value)))
(defmethod print-items:print-items append ((object value-mixin))
`((:value "= ~S" ,(value object))))
(defclass binding (value-mixin name-mixin)
())
(defmethod print-object ((object binding) stream)
(print-unreadable-object (object stream :type t :identity t)
(print-items:format-items
stream (print-items:effective-print-items object))))
(princ-to-string (make-instance 'binding :name "foo" :value 5))
#<BINDING = 5"foo" {100B2448F3}>
(ignore-errors
(remove-method #'print-object (find-method #'print-object '() (list (find-class binding) (find-class 't)))))
This solves the problem of composability and getting all output
parts between the prefix and suffix produced by
print-unreadable-object
, but the arrangement of output parts is
not ideal. We could improve the situation by tweaking the order of
elements in the superclass list of binding
but that would be
intrusive and again not composable when, for example, subclasses of
binding
are defined. Furthermore, the print-object
method does
not do anything specific to binding
.
The following adjustments solve both issues (changes in upper case):
(defmethod print-items:print-items append ((object value-mixin))
`(((:value (:AFTER :NAME)) " = ~S" ,(value object))))
(defclass binding (name-mixin value-mixin PRINT-ITEMS:PRINT-ITEMS-MIXIN)
())
;; no PRINT-OBJECT method for BINDING
(princ-to-string (make-instance 'binding :name "foo" :value 5))
#<BINDING "foo" = 5 {100B54C8D3}>
Constraints such as (:after :name)
control the order of
items. Constraints referring to absent items have no
effect. Contradictory constraints cause an error to be signaled.
2.3Advanced Usage
2.3.1Adjusting Items
It is sometimes necessary to modify or suppress the print items produced for superclasses to get the desired printed representation. This can be achieved in two ways:
- By defining a
print-items:print-items append
method thatreturns replacements for the undesired items:(defclass unnamed-binding (binding) ()) (defmethod print-items:print-items append ((object unnamed-binding)) `((:name "«unnamed»"))) (princ-to-string (make-instance 'unnamed-binding :name nil :value 5))
#<UNNAMED-BINDING «unnamed» = 5 {100B985D33}>
(ignore-errors (remove-method #'print-items:print-items (find-method #'print-items:print-items '(append) (list (find-class 'unnamed-binding)))))
- By defining a
print-items:print-items :around
method thatexplicitly modifies the complete item list:(defclass unnamed-binding (binding) ()) (defmethod print-items:print-items :around ((object unnamed-binding)) (remove :name (call-next-method) :key #'first)) (princ-to-string (make-instance 'unnamed-binding :name nil :value 5))
#<UNNAMED-BINDING = 5 {1006D45013}>
(ignore-errors (remove-method #'print-items:print-items (find-method #'print-items:print-items '(:around) (list (find-class 'unnamed-binding)))))
2.3.2Formatting Items
When it is necessary to take full control of item formatting, the
functions utilities.print-items:format-item
and
utilities.print-items:format-items
can be used:
(defclass custom-printing-binding (binding)
())
(defmethod print-object ((object custom-printing-binding) stream)
(print-unreadable-object (object stream :type t :identity t)
(let ((items (utilities.print-items:effective-print-items object)))
(format stream "my name is ~/utilities.print-items:format-item/, ~
my value is ~/utilities.print-items:format-item/, ~
the normal format would be ~
|~/utilities.print-items:format-items/|"
(find :name items :key #'utilities.print-items::parse-item)
(find :value items :key #'utilities.print-items::parse-item)
items))))
(princ-to-string (make-instance 'custom-printing-binding :name "name" :value 5))
#<CUSTOM-PRINTING-BINDING my name is "name", my value is = 5, the normal format would be |"name" = 5| {100C88B633}>
3Reference
The utilities.print-items
system provides the following protocol
for composable printing:
print-items:print-items OBJECT [generic function]
Return a list of items that should appear in the printed representation of
OBJECT
.Each method should return a list of items of the form
ITEM ::= (KEY-AND-OPTIONS FORMAT-CONTROL ARGUMENT*) KEY-AND-OPTIONS ::= KEY | (KEY OPTION*) KEY ::= any Lisp object OPTION ::= CONSTRAINT CONSTRAINT ::= ((:before | :after) KEY) FORMAT-CONTROL ::= `nil' | a format control string or a formatter function ARGUMENT ::= any Lisp object
When multiple items have
cl:eql
=KEY= s, items appearing closer to the beginning of the item list take precedence. This mechanism can be used by subclasses to replace print items produced by superclasses.When
FORMAT-CONTROL
isnil
, the whole item is ignored. This mechanism can be used by subclasses to disable print items produced by superclasses.print-items:print-items-mixin [class]
This mixin class adds printing via
print-items
to classes.Subclasses can define methods on
print-items:print-items
to change or extend the printed representation.print-items:format-item STREAM ITEM &optional COLON? AT? [function]
This utility function prints a single item in the format constructed by the
print-items
function to a stream.print-items:format-items STREAM ITEMS &optional COLON? AT? [function]
This utility function prints items in the format constructed by the
print-items
function to a stream.It is used to implement the
cl:print-object
method forprint-items-mixin
.