The less-familiar parts of Lisp for beginners — make-load-form-saving-slots

Having just presented a brief introduction to make-load-form, we go on to make-load-form-saving-slots.  This will be more of a continuation of the previous article, rather than a new discussion, so I’d recommend that you review that one before reading this text.

The example I gave in make-load-form was quite simple, and in fact lends itself well to using make-load-from-saving-slots.  In effect, make-load-form-saving-slots is a helper function that simplifies the writing of make-load-form functions.  If the class can be regenerated simply by recopying the contents of its slots, then make-load-form-saving-slots takes care of that.  So, let’s go back and look at our 2dpt class, and see what happens when we feed it to make-load-form-saving-slots.
 

CL-USER> (make-load-form-saving-slots (make-instance '2dpt))
(ALLOCATE-INSTANCE (FIND-CLASS '2DPT))
(PROGN
 (SETF (SLOT-VALUE #<2DPT {100323A443}> 'X) '0.0d0)
 (SETF (SLOT-VALUE #<2DPT {100323A443}> 'Y) '0.0d0))

Now, it may not be clear here, but this is returning two values.  So, this series of posts is directed at newcomers to Lisp, and even so I imagine you’ve encountered multiple return values before, but just in case this is new, it’s time for a short digression.

Forms in Lisp return zero or more values.  The common construction returns exactly one value, but by use of the values accessor it is possible to return 0 values, or several.  In many contexts, only the first value, the primary value, will be retained, but with macros like multiple-value-bind and multiple-value-list it is possible to recover all of the values returned from the form.

The example we gave of make-load-form returned only one value, so the second value is implicitly nil, no action.

OK, with that aside out of the way, why is my form so different than this generated form?  I’ll put them together for comparison:
 

CL-USER> (values (make-load-form (make-instance '2dpt)) 
                 nil)
(MAKE-INSTANCE '2DPT :X 0.0d0 :Y 0.0d0)
NIL
CL-USER> (make-load-form-saving-slots (make-instance '2dpt))
(ALLOCATE-INSTANCE (FIND-CLASS '2DPT))
(PROGN
 (SETF (SLOT-VALUE #<2DPT {10036A2263}> 'X) '0.0d0)
 (SETF (SLOT-VALUE #<2DPT {10036A2263}> 'Y) '0.0d0))

In fact, make-load-form returns two values.  The first value is the “creation form”, the second is the “initialization form”.  Creation forms may not contain circular references, while initialization forms may, so there is value in splitting them out this way.  For the simple case here, the end result is the same, but for more complicated cases the distinction is important.  You’ll note, also, that my simple implementation uses make-instance, while the other one uses allocate-instance.  You should review the discussion under allocate-instance to understand the difference, but in C++ terms make-instance calls constructor-like functions while allocate-instance simply sets aside the storage.

Now, I mentioned circular references.  What do I mean, and what is the impact?  Well, let’s look at the dl-list structure I created while discussing macros a long time ago.  I’m not going to reproduce the whole file here, just some definitions to provide context.  Please review the earlier article for the full source code.
 

(defpackage :DL-LIST
  (:use :COMMON-LISP)
  (:export :EMPTY-P :DL-LENGTH :ITER-FRONT :ITER-BACK 
           :ITER-NEXT :ITER-PREV :PUSH-FRONT :PUSH-BACK
           :POP-FRONT :POP-BACK :ITER-CONTENTS
           :INSERT-AFTER-ITER :MAKE-DL-LIST))

(in-package :DL-LIST)

;; Define the node.  They're opaque types to the user, but 
;; they double as iterators (also opaque).
(defstruct (dl-node)
  (value        nil)
  (next-node    nil)
  (prev-node    nil))

;; Define the doubly-linked list.
(defstruct (dl-list (:conc-name dlst-))
  (first-node   nil)
  (last-node    nil))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;;   Etcetera
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Now, there are two ways we can make a load form for a dl-list.  One is the brute-force way, recreating the structures by filling the slots with their appropriate values, by making a load form for both dl-list and dl-node:
 

(defmethod make-load-form ((obj dl-list) &optional environment)
  (declare (ignore environment))
  `(dl-list:make-dl-list :first-node ',(dlst-first-node obj)
                         :last-node ',(dlst-last-node obj)))

(defmethod make-load-form ((obj dl-node) &optional environment)
  (declare (ignore environment))
  (values
   `(dl-list::make-dl-node)
   `(setf (dl-list::dl-node-value) ,(dl-list::dl-node-value obj)
          (dl-list::dl-node-next-node) ,(dl-list::dl-node-next-node obj)
          (dl-list::dl-node-prev-node) ,(dl-list::dl-node-prev-node obj))))

This works:
 

CL-USER> (defparameter *dlist* (dl-list:make-dl-list))
*DLIST*
CL-USER> (dl-list:push-front *dlist* 10)
{ NIL <- 10 -> NIL }
CL-USER> (dl-list:push-front *dlist* 9)
{ NIL <- 9 -> 10 }
CL-USER> *dlist*
#S(DL-LIST::DL-LIST
   :FIRST-NODE { NIL <- 9 -> 10 }
   :LAST-NODE { 9 <- 10 -> NIL })
CL-USER> (make-load-form *dlist*)
(DL-LIST:MAKE-DL-LIST :FIRST-NODE '{ NIL <- 9 -> 10 } :LAST-NODE
                      '{ 9 <- 10 -> NIL })
CL-USER> (eval (make-load-form *dlist*))
#S(DL-LIST::DL-LIST
   :FIRST-NODE { NIL <- 9 -> 10 }
   :LAST-NODE { 9 <- 10 -> NIL })
CL-USER> (eq *dlist* (eval (make-load-form *dlist*)))
NIL

As you can see, I create a new dl-list and fill it with the sequence 9,10.  Then I make a load form from it, and eval it (I can do this because the dl-list load form does not have an initialization form, only a creation form).  The result of the eval is a new dl-list, with the same contents, but not eq to the original one.  So, I’ve created a copy of the dl-list.  It’s important that the filling in of the slots in the dl-node structures be done in the initialization form, not the creation form, because of the circularity that results from following the next pointer of the previous pointer of a node.

However, there is another way to make the load forms, one that’s arguably better.  The other way relies on the external API of the dl-list structures, rather than their internal implementations:
 

(defmethod make-load-form ((obj dl-list) &optional environment)
  (declare (ignore environment))
  (values
   `(dl-list:make-dl-list)
   `(progn
      ,@(let (contents)
             (iter-loop (obj iter)
               (push (iter-contents obj iter) contents))
             (mapcar #'(lambda (x)
                         `(dl-list:push-front ,obj ,x))
                     contents)))))

What this does is builds a load form by reading out the contents of the dl-list, and then returning a form that inserts into the dl-list using the external symbols of the package.
 

CL-USER> (defparameter *dlist* (dl-list:make-dl-list))
*DLIST*
CL-USER> (dl-list:push-front *dlist* 10)
{ NIL <- 10 -> NIL }
CL-USER> (dl-list:push-front *dlist* 9)
{ NIL <- 9 -> 10 }
CL-USER> (make-load-form *dlist*)
(DL-LIST:MAKE-DL-LIST)
(PROGN
 (DL-LIST:PUSH-FRONT
  #S(DL-LIST::DL-LIST
     :FIRST-NODE { NIL <- 9 -> 10 }
     :LAST-NODE { 9 <- 10 -> NIL })
  10)
 (DL-LIST:PUSH-FRONT
  #S(DL-LIST::DL-LIST
     :FIRST-NODE { NIL <- 9 -> 10 }
     :LAST-NODE { 9 <- 10 -> NIL })
  9))

That means that I can change the internal implementation of the dl-list and dl-node structures, and as long as I don’t change the API manipulators, this make-load-form function will continue to work.  I’ve used an initialization form separate from the creation form in case a dl-list contains references to other objects that refer back to the dl-list.  That would be a circular reference and would cause an error if all of the dl-list:push-front invocations were in the creation form.  One inconvenience, though, is that eval at the REPL won’t demonstrate the correctness of this because it will only evaluate the creation forms by default.  So, to show that this is working, I add a form at the bottom of the input file and compile it:
 

(let ((test-case 
       #.(let ((rv (dl-list:make-dl-list)))
           (dl-list:push-front rv
                               (get-universal-time))
           (dl-list:push-front rv
                               9)
           rv)))
  (format t "~A~%" test-case))

This added form will cause the Lisp instance to generate output when the file is loaded.  To show that the object was copied from its form at compile time, here it is in context:
 

CL-USER> (progn
           (format t "~D~%" (get-universal-time))
           (load "dllist-v5")
           (format t "~D~%" (get-universal-time)))
3603308753
#S(DL-LIST
   :FIRST-NODE { NIL <- 9 -> 3603308724 }
   :LAST-NODE { 9 <- 3603308724 -> NIL })
3603308753
NIL

So, that’s the second half of our discussion of make-load-form.  As we showed, make-load-form-saving-slots is useful, in certain fairly simple applications, but the more general make-load-form, properly used, can help to insert objects generated once, at compile time, and so avoid the cost of regenerating those objects at load-time or run-time.

2 thoughts on “The less-familiar parts of Lisp for beginners — make-load-form-saving-slots

  1. Where does the iter in the dl-list make-load-form come from? It spontaneously appears without having been initialized. Since the make-load-form works correctly, I am wondering how iter is actually initialized.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

反垃圾邮件 / Anti-spam question * Time limit is exhausted. Please reload CAPTCHA.