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

We continue this series of Lisp features which the newcomer from C++ might not have encountered with make-load-form.  This is a standard generic function, like print-object, and like print-object, the programmer may find contexts in which it is helpful to specialize this function on a programmer-defined class.

If you recall the earlier article on load-time-value, we had a way for the programmer to build objects as the Lisp code was being read into the Lisp instance, rather than at execution time.  In this way, objects can be built once, as the code is loaded, rather than every time the form is encountered during normal execution.  However, there is another option.  What if the object could be built during compilation?  Now, you don’t even need to incur the cost of building the instances at load time, you can generate the instances once, as the code is being compiled, and then not have to incur the cost of regenerating the instances every time the Lisp files are loaded into a Lisp instance.

So, one way you might do this is with the read-macro #..  That’s typographically awkward, it’s the ‘#’ character followed by a single period.  This read-macro evaluates the form that follows it and inserts the result into the code at that point.  Here’s an example of this use:
 

(defun times ()
  (let ((current-time (get-universal-time))
        (read-time #.(get-universal-time)))
    (format t "Current time= ~D~%" current-time)
    (format t "Read time= ~D~%" read-time)))

When this defun form is passed to the Lisp instance, an interesting thing happens.  The #. read-macro causes the form (get-universal-time) to be evaluated at read time, when the defun is encountered, and the returned value is inserted as a literal constant into the code.  That means that, as far as the function is concerned, current-time is assigned at invocation time, by calling a function, but read-time is assigned from an integer constant, one that does not change as you call the function several times.  Here’s an example output:
 

CL-USER> (dotimes (i 3)
           (format t "-----------------~%")
           (sleep 2)
           (times))
-----------------
Current time= 3603210003
Read time= 3603209874
-----------------
Current time= 3603210005
Read time= 3603209874
-----------------
Current time= 3603210007
Read time= 3603209874
NIL

As you can see, the read time remains fixed even as the current time is updated.

So, this works well.  If you compile the file, the read-time variable is fixed to the time when the form was read in to begin the compilation of that function.  The read-time has effectively become the compile time.  Now, what if you wanted to do this with an object?  I’ll go through this slowly, starting with some incorrect examples and explaining the problems as we go.

We define our own class of 2-dimensional points and build instances of them at read time.  Here’s the file:
 

(defclass 2dpt ()
  ((x           :accessor get-x
                :initarg :x
                :initform 0.0d0)
   (y           :accessor get-y
                :initarg :y
                :initform 0.0d0)))

(defun distance (pt1 pt2)
  (let ((delta-x (- (get-x pt2) (get-x pt1)))
        (delta-y (- (get-y pt2) (get-y pt1))))
    (sqrt (+ (* delta-x delta-x) (* delta-y delta-y)))))

(defun demonstrate ()
  (let ((offset #.(make-instance '2dpt :x 0.0d0 :y 0.0d0))
        (testpt #.(make-instance '2dpt :x 1.0d0 :y 2.0d0)))
    (format t "Distance= ~F~%" (distance testpt offset))))

We can load this .lisp file, and run the demonstrate function, it seems fine.  So, now we decide it’s time to compile it in a fresh Lisp instance:
 

CL-USER> (compile-file "make-load-form.lisp")
; compiling file "make-load-form.lisp" (written 08 MAR 2014 02:52:16 PM):
; compiling (DEFCLASS 2DPT ...)
; compiling (DEFUN DISTANCE ...)
; 
; caught ERROR:
;   READ error during COMPILE-FILE:
;   
;     There is no class named COMMON-LISP-USER::2DPT.
;   
;     (in form starting at line: 12, column: 57, file-position: 364)
; 
; compilation unit aborted
;   caught 1 fatal ERROR condition
;   caught 1 ERROR condition
; compilation aborted after 0:00:00.011
NIL
T
T

So, what happened?  Why is this an error?  Well, we’re compiling the file, not loading it.  The defclass form is not evaluated, so when the read-macro calls make-instance on the 2dpt class, there is no record of a class by that name in the Lisp image.  That results, naturally, in an error.

So, we need to make the defclass visible to the compiler.  We covered eval-when earlier in this series.  Here’s the modification:
 

(eval-when (:compile-toplevel :load-toplevel)
  (defclass 2dpt ()
    ((x         :accessor get-x
                :initarg :x
                :initform 0.0d0)
     (y         :accessor get-y
                :initarg :y
                :initform 0.0d0))))

(defun distance (pt1 pt2)
  (let ((delta-x (- (get-x pt2) (get-x pt1)))
        (delta-y (- (get-y pt2) (get-y pt1))))
    (sqrt (+ (* delta-x delta-x) (* delta-y delta-y)))))

(defun demonstrate ()
  (let ((offset #.(make-instance '2dpt :x 0.0d0 :y 0.0d0))
        (testpt #.(make-instance '2dpt :x 1.0d0 :y 2.0d0)))
    (format t "Distance= ~F~%" (distance testpt offset))))

Now, we compile the file:
 

CL-USER> (compile-file "make-load-form.lisp")
; compiling file "make-load-form.lisp" (written 08 MAR 2014 03:02:00 PM):
; compiling (DEFCLASS 2DPT ...)
; compiling (DEFUN DISTANCE ...)
; compiling (DEFUN DEMONSTRATE ...)

; file: make-load-form.lisp
; in: DEFUN DEMONSTRATE
;     (LET ((OFFSET #<2DPT {1005CAF253}>) (TESTPT #<2DPT {1005CB3A43}>))
;       (FORMAT T "Distance= ~F~%" (DISTANCE TESTPT OFFSET)))
; ==>
;   #<2DPT {1005CAF253}>
; 
; caught ERROR:
;   don't know how to dump #<2DPT {1005CAF253}> (default MAKE-LOAD-FORM method called).

; ==>
;   #<2DPT {1005CB3A43}>
; 
; caught ERROR:
;   don't know how to dump #<2DPT {1005CB3A43}> (default MAKE-LOAD-FORM method called).
; 
; note: deleting unreachable code
; 
; note: deleting unreachable code
; 
; compilation unit finished
;   caught 2 ERROR conditions
;   printed 2 notes

; make-load-form.fasl written
; compilation finished in 0:00:00.013
#P"make-load-form.fasl"
T
T

What happened this time?  Well, we’ve written code to build 2dpt objects at read time during the compilation.  Those objects are to be inserted directly into the function where they appear.  However, the compiler is responsible for producing code that can be read in later, in a different Lisp image, so it has to know how to store instances of the 2dpt object in such a way that they can be reliably reconstructed during the later load.  It can’t just deposit a binary copy of the instance, there might be references to other objects inside it, and those references can’t generally be bitwise-copied across instances.  In C++ terms, if you are attempting to store an object to disk and expect to be able to re-read it in a later run of the program, you’re going to have to figure out what to do about pointers in the object, which will almost certainly be nonsensical when the object is loaded later.  This is the same reasoning that applies when you write copy-constructors in C++.

So, think of make-load-form as a kind of way of defining copy-constructors for use by the compiler.  Now, we insert that into the code and try again:
 

(eval-when (:compile-toplevel :load-toplevel)
  (defclass 2dpt ()
    ((x         :accessor get-x
                :initarg :x
                :initform 0.0d0)
     (y         :accessor get-y
                :initarg :y
                :initform 0.0d0)))

  (defmethod make-load-form ((obj 2dpt) &optional environment)
    (declare (ignore environment))
    `(make-instance '2dpt :x ,(get-x obj) :y ,(get-y obj))))

(defun distance (pt1 pt2)
  (let ((delta-x (- (get-x pt2) (get-x pt1)))
        (delta-y (- (get-y pt2) (get-y pt1))))
    (sqrt (+ (* delta-x delta-x) (* delta-y delta-y)))))

(defun demonstrate ()
  (let ((offset #.(make-instance '2dpt :x 0.0d0 :y 0.0d0))
        (testpt #.(make-instance '2dpt :x 1.0d0 :y 2.0d0)))
    (format t "Distance= ~F~%" (distance testpt offset))))

Now, we compile the code again, and this time it compiles cleanly, without errors or warnings.  And that’s a basic look at make-load-form, but as this is getting long, we’ll look at it in more detail in the next article on make-load-form-saving-slots, where we will see some more advanced behaviours that are interesting to go over.

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.