This answer uses Common Lisp for its examples, but it is not fundamentally different in Scheme. Note also that you can play with sdraw.lisp in Common Lisp (e.g. with CCL or SBCL programs) if you want to see how printing the diagram can actually be implemented.
You first have to be clear about the result you want to print, which might be difficult when the source contains cons/list/append operations. Also, since the source code is also a tree of cons-cells, you have to take care not to mix the source with the value obtained after its evaluation.
So it all starts with evaluating the form correctly.
Evaluate cal
Let's first evaluate the expression. Below, I also mention drawing the boxes directly from the input expression, but IMO it helps to detail that intermediate step.
The result is the same in Scheme and Common Lisp, after recursively evaluating all expressions:
((((1 2) 3) (4 5)) 6 7)
Here is how you could ask the system to trace the computation, in Common Lisp. First of all, know that you cannot trace standard functions like list
, etc. So let's shadow them in a custom package with simple wrappers:
(defpackage :so
(:use :cl)
(:shadow #:list #:cons #:append))
(in-package :so)
(defun list (&rest args) (apply #'cl:list args))
(defun cons (&rest args) (apply #'cl:cons args))
(defun append (&rest args) (apply #'cl:append args))
Then, in the REPL, go to that package:
CL-USER> (in-package :so)
#<PACKAGE "SO">
Ask to trace those functions:
SO> (trace list append cons) ;; the shadowing ones
(LIST CONS APPEND)
Now, you can enter the value of cal
directly, but this time the symbols being used are the one we asked to trace.
SO> (list (append (list (cons (list 1 2) (cons 3 '())))
(list (cons 4 (cons 5 '()))))
6
7)
The environment then evaluates the form and prints how each function is called and what results it returns.
0: (SO::LIST 1 2)
0: LIST returned (1 2)
0: (SO::CONS 3 NIL)
0: CONS returned (3)
0: (SO::CONS (1 2) (3))
0: CONS returned ((1 2) 3)
0: (SO::LIST ((1 2) 3))
0: LIST returned (((1 2) 3))
0: (SO::CONS 5 NIL)
0: CONS returned (5)
0: (SO::CONS 4 (5))
0: CONS returned (4 5)
0: (SO::LIST (4 5))
0: LIST returned ((4 5))
0: (SO::APPEND (((1 2) 3)) ((4 5)))
0: APPEND returned (((1 2) 3) (4 5))
0: (SO::LIST (((1 2) 3) (4 5)) 6 7)
0: LIST returned ((((1 2) 3) (4 5)) 6 7)
((((1 2) 3) (4 5)) 6 7)
Vizualize as cons cells
It can be helpful to view lists as chains of cons-cells, i.e. turn (a b c)
into (a . (b . (c . nil)))
. Let's define a helper function:
(defun consprint (x)
(if (consp x)
(format nil
"(~a . ~a)"
(consprint (car x))
(consprint (cdr x)))
(prin1-to-string x)))
Here is the result:
SO> (consprint '((((1 2) 3) (4 5)) 6 7))
"((((1 . (2 . NIL)) . (3 . NIL)) . ((4 . (5 . NIL)) . NIL)) . (6 . (7 . NIL)))"
Draw: a term rewriting approach
Use a recursive, bottom-up approach to draw it.
Definition.: Here I define a leaf as a cons-cell that has atoms in both its CAR and its CDR slots: e.g. (0 . NIL)
and (X . Y)
are both leaves, but not ((0 . 1) . 2)
. Note that this includes improper lists, something I am relying on to explain the drawing method when I replace subterms by symbols.
((((1 . (2 . NIL)) . (3 . NIL)) . ((4 . (5 . NIL)) . NIL)) . (6 . (7 . NIL)))
^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^
Above I underlined all leaves: you can easily draw those boxes, and label them with symbols (A, B, ...).
Here below I replace the original cells with the names of their associated boxes, and again underline the new leaves:
((((1 . A) . B) . ((4 . C) . NIL)) . (6 . D))
^^^^^^^ ^^^^^^^ ^^^^^^^
Then, when there is a symbol representing a box, draw an arrow to that box. For example, you define a box named E that corresponds to (1 . A)
, so you draw [1/x]
and connect the x
to box A
.
You obtain:
(((E . B) . (F . NIL)) . G)
Now, consider (E . B)
: its car is a symbol, so the box you need to draw has no value, but an outgoing arrow from the CAR slot that points to the E
cell (just like its CDR points to B
).
You repeat this process until termination. The rest is visual layout, where typically boxes that are just linked lists of atoms are laid out horizontally.
Directly drawing from expressions
Presumably the original exercise wants you to draw boxes right from the original expression. The same can be done as above, working upward from leaf expressions and replacing their values by symbols denoting existing boxes.
- A cons maps directly to a box.
- a list is just a repeated application of cons, and you can go quickly by drawing as many boxes as there are elements.
- append in practice makes a copy of all but the last list in arguments, but when drawing you can "mutate" the existing boxes. For each existing box, keep following the CDR until you reach a box with no arrow in cdr, and link that box to the next one in the arguments, thus chaining the different boxes together.
It can be interesting to also draw the actual, purely functional version of append to see how structure sharing and garbage collection work.