Tag Archives: programming

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.

The less-familiar parts of Lisp for beginners — make-instances-obsolete

Our next stop in the Lisp features that newcomers arriving from C++ might not be aware of is make-instances-obsolete.  We’ve touched on this capacity briefly before, in this article.

Once again, this returns to a fundamental difference between C++ programs and Lisp programs, one that I’ve mentioned several times before in this series, but which I will repeat here.

C++ programs are written, compiled, and executed.  Barring implementation-defined tricks like shared object plugins, if the C++ programmer wants to make a change to the code, he edits the source, recompiles it, and then shuts down the running version of the program, and then restarts the program using the changed binary.

In Lisp, functions and data are added to a Lisp instance, and the “program” can be modified by updating functions in the Lisp, without shutting it down and restarting.  One possible change is to modify the definition of a class, which can be done even if there are instances of the class present in the Lisp instance.  When the class is modified in such a way as to change the slots (the equivalent of members in C++ structs or classes), then make-instances-obsolete is automatically invoked on the class, and all instances are marked for updating.  At some implementation-defined time later, but before the next access to a slot in a specific instance, the method update-instance-for-redefined-class is called on the instance to make any required changes to bring an instance constructed in the old definition into compliance with the new definition of its class.  The programmer can also call make-instances-obsolete explicitly, in those cases where the automatic invocation would not be performed because the slots are unchanged.

The less-familiar parts of Lisp for beginners — make-dispatch-macro-character

We next arrive at make-dispatch-macro-character.  I would suggest that the new Lisp user review the discussion on readtables and read-macros that can be found in the gensym and get-dispatch-macro-character articles.

The make-dispatch-macro-character function creates a new dispatching macro character (i.e. the first character of two-character read macros) for the read table.  It is initially constructed with all sub-characters set to signal errors.  Once make-dispatch-macro-character has built this new dispatch, the individual sub-characters can be assigned with set-dispatch-macro-character.

The less-familiar parts of Lisp for beginners — make-broadcast-stream

Next, we come to make-broadcast-stream.  This isn’t really a difficult concept to understand, but I highlight it because it’s not a primitive that appears in the C library, so a new Lisp programmer coming from C++ might not realize that this tool is available.  This function creates an output stream that transparently splits its data streams out into one or more target streams.  The most obvious uses for this are for setting up a mode that echos data to the screen as well as its normal output target, or for creating a unified transcript of client and server messages for debugging purposes.  Naturally, any other context where you want to send the same data to multiple output streams also benefits from this function.

Earlier in this series I skipped broadcast-stream-streams as I was waiting to include it with make-broadcast-stream.  The broadcast-stream-streams function returns a list of the streams to which the broadcast stream sends its output.