I think I did this right in the past, but this time, I am kind of stuck...
The code below is the CLOS style structure of a rope. Which in essence is a binary tree.
For printing purposes, I would like the left
and right
properties to recursively indent by 2 spaces.
(defclass rope ()
((depth :type fixnum :initform 0 :initarg :depth :reader rope-depth)
(weight :type fixnum :initform 0 :initarg :weight :reader rope-weight)))
(defclass rope-leaf (rope)
((string :type string :initarg :string :reader rope-string)))
(defclass rope-node (rope)
((left :type rope :initarg :left :reader rope-left)
(right :type rope :initarg :right :reader rope-right)))
(defmethod print-object ((object rope) stream)
(print-unreadable-object (object stream :type t)
(with-slots (depth weight) object
(format stream "depth: ~a weight: ~a" depth weight))))
(defmethod print-object ((object rope-node) stream)
(call-next-method)
(with-slots (left right) object
(format stream "~<~%left: ~a ~%right: ~a~>" left right)))
(defmethod print-object ((object rope-leaf) stream)
(print-unreadable-object (object stream :type t)
(call-next-method)
(format stream " ~s" (rope-string object))))
The problem is with the print-object
implementation for rope-node
.
I read the documentation, but could not find an intuitive example for nested (recursive) structures with a tree-like indent. I tinkered for a (longer while), trying myself. Eventually, I asked googles Gemini AI for help, but it passed with:
It seems I'm having trouble correctly applying these specific format control directives within the print-object context. I'm still under development and learning to handle these nuances of Common Lisp's format function.
Now is the time for humanity to shine ;) Who can give a solution and explain, how such nested prints are supposed to work?
I think I did this right in the past, but this time, I am kind of stuck...
The code below is the CLOS style structure of a rope. Which in essence is a binary tree.
For printing purposes, I would like the left
and right
properties to recursively indent by 2 spaces.
(defclass rope ()
((depth :type fixnum :initform 0 :initarg :depth :reader rope-depth)
(weight :type fixnum :initform 0 :initarg :weight :reader rope-weight)))
(defclass rope-leaf (rope)
((string :type string :initarg :string :reader rope-string)))
(defclass rope-node (rope)
((left :type rope :initarg :left :reader rope-left)
(right :type rope :initarg :right :reader rope-right)))
(defmethod print-object ((object rope) stream)
(print-unreadable-object (object stream :type t)
(with-slots (depth weight) object
(format stream "depth: ~a weight: ~a" depth weight))))
(defmethod print-object ((object rope-node) stream)
(call-next-method)
(with-slots (left right) object
(format stream "~<~%left: ~a ~%right: ~a~>" left right)))
(defmethod print-object ((object rope-leaf) stream)
(print-unreadable-object (object stream :type t)
(call-next-method)
(format stream " ~s" (rope-string object))))
The problem is with the print-object
implementation for rope-node
.
I read the documentation, but could not find an intuitive example for nested (recursive) structures with a tree-like indent. I tinkered for a (longer while), trying myself. Eventually, I asked googles Gemini AI for help, but it passed with:
It seems I'm having trouble correctly applying these specific format control directives within the print-object context. I'm still under development and learning to handle these nuances of Common Lisp's format function.
Now is the time for humanity to shine ;) Who can give a solution and explain, how such nested prints are supposed to work?
Share Improve this question edited Feb 14 at 16:00 Barmar 782k56 gold badges546 silver badges660 bronze badges asked Feb 14 at 15:50 BitTicklerBitTickler 11.9k6 gold badges36 silver badges59 bronze badges 7 | Show 2 more comments1 Answer
Reset to default 2Example tree
Let's first define constructors and have an example tree:
(defun node (a b)
(make-instance 'rope-node :left a :right b))
(defun leaf (s)
(make-instance 'rope-leaf :string s))
(defparameter *test*
(node (node (leaf "a")
(leaf "b"))
(node (leaf "c")
(node (leaf "d")
(leaf "e")))))
Explicit formatter
When I am in doubt about the FORMAT
function, I try first to use the pprint-logical-block
facility. Here I'm building a list of children as a second argument to the form, so that I can iterate over them. Using NIL
instead and accessing them explicitly is another possible approach. But using another object, like o
, instead of a list, would bypass the block completely (that used to be the confusing part to me).
(defmethod print-object ((o rope-node) stream)
(pprint-logical-block (stream (list (rope-left o) (rope-right o)))
(pprint-indent :block 2 stream)
(format stream "#<rope-node")
(loop
(pprint-exit-if-list-exhausted)
(format stream " ")
(pprint-newline :linear stream)
(print-object (pprint-pop) stream)))
(format stream ">"))
I also tried to combine it with print-unreadable-object
but the function adds its own indent, which obscures things a little bit. Here I define explicitly the indent, before starting writing anything, so that indent starts at the position of the #<
sequence of characters. Thanks to the list of children, I can use pprint-pop
and pprint-exit-if-list-exhausted
to iterate on them. Notice how I print both a space and a newline (which removes the space if needed) when I know that there is at least one remaining element. The linear style of newline helps printing either all child nodes on the same line, or each on a distinct line.
With the above definition (print *test*)
outputs the following:
#<rope-node
#<rope-node #<ROPE-LEAF {1021F704E3}> #<ROPE-LEAF {1021F704F3}>>
#<rope-node
#<ROPE-LEAF {1021F70503}>
#<rope-node #<ROPE-LEAF {10222B0503}> #<ROPE-LEAF {10222B0513}>>>>
I can also define a method for class rope-leaf
:
(defmethod print-object ((o rope-leaf) stream)
(format stream "[~a]" (rope-string o)))
Now, the output fits in a single line:
#<rope-node #<rope-node [a] [b]> #<rope-node [c] #<rope-node [d] [e]>>>
Using FORMAT
Now that I know the structure of my formatter a little better, it should be easy to translate it to the character soup dedicated syntax of format
, let's try:
(defmethod print-object ((o rope-node) stream)
(format stream
"~@<~2I#<node~{~^ ~_~a~}~:>>"
(list (rope-left o) (rope-right o))))
With some trial and errors, the above is similar to the previous definition, you have to remember that ~<...~:>
accepts a list of arguments, but that you can use ~@<...~:>
to pass the rest of the arguments given to FORMAT
instead. Inside the block, I also need to iterate over a list of children using ~{...~}
, which is why I build a list. But, the ~@{...~}
syntax also allows you to take the next available arguments, meaning that the following works too:
(defmethod print-object ((o rope-node) stream)
(format stream
"~@<~2I#<node~@{~^ ~_~a~}~:>>"
(rope-left o)
(rope-right o)))
The example above prints as:
#<node #<node [a] [b]> #<node [c] #<node [d] [e]>>>
Readability
Let's just tweak the printers a bit:
(defmethod print-object ((o rope-leaf) stream)
(with-slots ((s string)) o
(format stream "(~s ~s)" 'leaf s)))
(defmethod print-object ((o rope-node) stream)
(with-slots ((L left) (R right)) o
(format stream "~@<~2I(~s~@{~^ ~_~a~}~:>)" 'node L R)))
I switch to with-slots
with the binding syntax where the slots are given shorter names, and I am emitting a Lisp form as an output. That makes the output readable, and since the node
and leaf
symbols are written using ~s
, they are going to be prefixed by the home package if necessary.
(let ((*print-right-margin* 20))
(progn (print *test*) (values)))
(node
(node
(leaf "a")
(leaf "b"))
(node
(leaf "c")
(node
(leaf "d")
(leaf "e"))))
The default printer for lists wouldn't respect the formatting you may want to implement, but if you don't care too much its usually enough to convert to a readable list when printing, like this:
(defmethod print-object ((o rope-leaf) stream)
(print-object `(leaf ,(rope-string o)) stream))
(defmethod print-object ((o rope-node) stream)
(print-object `(node ,(rope-left o) ,(rope-right o)) stream))
~:>
to end the directive to make this be logical blocks. If you end with~>
it's the justification operation. – Barmar Commented Feb 14 at 16:05(format stream "..." (list left right))
as it seems to complain then about too many arguments. Or do I need 2 logical blocks? – BitTickler Commented Feb 14 at 16:08~@<
to make it process the rest of the arguments. – Barmar Commented Feb 14 at 16:10