The less-familiar parts of Lisp for beginners — eval-when

Next in our review of less-commonly seen Lisp features is eval-when.  This is a bit of a subtle one, that has to do with executing certain forms in certain circumstances.

There are three arguments you might use to eval-when:compile-toplevel, :load-toplevel, and :execute.  The first two apply only in situations that involve compiling Lisp code.  This does not include entering a defun at the command line, or even loading a .lisp file from disc.  It’s important to note that :load-toplevel applies only to the loading of compiled Lisp files, not to the loading of the source code in a .lisp disc file.

To understand things a bit, let’s talk about what happens when you load a .lisp file.  You may look at the file and think it looks like you’re telling it to load a bunch of functions and macros, but you should think of it more like running a shell script.  A set of commands that are immediately executed, in sequence.  Of course, when a form begins with defun or defmacro, it doesn’t do anything obvious, merely sets up a function or macro.  But if a toplevel form were something like a format output command, that would be executed, and the output appear on your screen as you load the .lisp file.  Loading a .lisp file looks like typing those commands into the Lisp prompt, in order.  So, as I said, think shell script, acting in a functioning Lisp environment.

Compiling a file, however, is different.  The compiler executes in a specially modified Lisp environment, scanning the input file and compiling functions, but deliberately not executing toplevel forms.  Once the file has been compiled, loading the resulting object file looks a lot like loading a .lisp file, toplevel forms are executed.  So, you can think of compile-file as compiling a script, and the following load operation as running the compiled script.

Sometimes, however, the programmer has some forms that he or she wants to have execute in the compilation environment, perhaps to modify the behaviour of the compiler.  This might be used for optimization settings, or maybe to record when a file was compiled.  Another interesting use is in modifying the language, and adjusting the compiler to handle it.

I’ve talked before about Lisp language features that I miss when I’m writing C++ code.  Well, there’s a feature of C/C++ that I miss in Lisp.  That is, preprocessor concatenation of string literals.  When I’m writing a long string literal, and don’t want the text to wrap, I can type in a sequence of string literals, enclosed in double-quotes and separated by blanks, knowing the the preprocessor will assemble them into a single long string.  This feature isn’t present in the Lisp reader.  Here, though, I’ll show how to implement it at compile time, so that the compiler can operate on files containing this modified Lisp syntax.

(format t "Hello there, this is a toplevel executing form.~%")

(eval-when (:compile-toplevel)

  (defparameter *rt-copy* (copy-readtable))

  (defun bar-reader (stream char)
    (declare (ignore char))
    (let ((stringlist (read stream t nil t)))
      (apply 'concatenate 'string stringlist)))

  (set-macro-character #\_ #'bar-reader)

  (multiple-value-bind (sec min hour day month year)
      (decode-universal-time (get-universal-time))
    (with-open-file (s "e-w-demo-compile-time.lisp"
                       :direction :output 
                       :if-exists :supersede)

      (format s "~S~%"
              `(unintern '+compile-time+))
      (format s "~S~%"
              `(defconstant +compile-time+
                (format nil 
                 "~2,'0D:~2,'0D:~2,'0D ~0,4D-~2,'0D-~2,'0D"
                 ,hour ,min ,sec ,year ,month ,day))))))

(eval-when (:compile-toplevel :load-toplevel)
  (load "e-w-demo-compile-time.lisp" :if-does-not-exist nil))

(defun demonstrate ()
    ((boundp '+compile-time+)
     (format t "File compiled at: ~A~%" +compile-time+))
     (format t "No compile time information available.~%")))
  (format t _("This is a long string that I don't "
              "want to see linewrap when formatting "
              "for the blog post~%")))

(eval-when (:compile-toplevel)
  (copy-readtable *rt-copy* *readtable*)
  (unintern '*rt-copy*))

The output here is:
CL-USER> (compile-file "eval-when")
; compiling file "/home/neufeld/programming/lisp/blogging/eval-when.lisp" (written 17 JAN 2014 19:59:43 AM):
; compiling (FORMAT T ...)
; compiling (LOAD "e-w-demo-compile-time.lisp" ...)
; compiling (DEFUN DEMONSTRATE ...)

; /home/neufeld/programming/lisp/blogging/eval-when.fasl written
; compilation finished in 0:00:00.018
CL-USER> (load "eval-when")
Hello there, this is a toplevel executing form.
CL-USER> (demonstrate)
File compiled at: 20:05:42 2014-01-17
This is a long string that I don't want to see linewrap when formatting for the blog post

Note that the format statement was not printed out when compiling the file, but was printed when loading it.  The compile-time modification to the reader that I made asks it, when presented with the ‘_’ (underline) character, to read the next object, which must be a list of strings, and return a string which is the concatenation of those in the list.  When this new syntax is used in the demonstrate function, it is recognized and understood.  Without the use of eval-when, this would not be possible, as the compiler deliberately avoids executing forms such as the one to modify the read syntax, unless specifically told otherwise.

What, exactly, is going on in this file?  Well, at compile time, a copy is made of the read table, so that we can restore it after we’ve finished using our modified read table.  That is to avoid polluting the Lisp image on which we’re performing the compilation, it’s bad form for the compile operation to modify the behaviour of the image after the compilation has completed.  Next, we set up our modified read table to concatenate strings, and we write out the current time to a file on disc.  This disc file allows us to record new forms generated at compile time but absent at load time.  The file is compiled, and then at the end of the compilation we return the read table to its former state.

I haven’t mentioned the final eval-when option.  The third option to eval-when, :execute, is the “everything else” category.  These forms are evaluated when neither in the compiler, nor loading a compiled file.

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.