utilities.print-items

2022-07-08

A protocol for flexible and composable printing.

Upstream URL

github.com/scymtym/utilities.print-items

Author

Jan Moringen <jmoringe@techfak.uni-bielefeld.de>

Maintainer

Jan Moringen <jmoringe@techfak.uni-bielefeld.de>

License

LGPLv3
README
utilities.print-items README

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 on print-object wouldproduce output parts outside of the prefix and suffix produced byprint-unreadable-object.
https://travis-ci.org/scymtym/utilities.print-items.svg

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:

  1. 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)))))
    
  2. 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 is nil, 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 for print-items-mixin.

Dependencies (2)

  • alexandria
  • fiveam
  • GitHub
  • Quicklisp