I'm learning Common Lisp from Practical Common Lisp. It has an example of helper functions for reading and writing binary files in Chapter 24. Here's one example:
(defun read-u2 (in)
(+ (* (read-byte in) 256) (read-byte in)))
I can write functions for reading other kinds of binary numbers likewise. But I thought that doing so violates the DRY principle. Besides, these functions are going to be similar, so I tried to generate the functions with macros.
(defmacro make-read (n be)
`(defun ,(intern (format nil "READ~d~:[L~;B~]E" n be))
(&optional (stream *standard-input*))
(logior ,@(loop for i from 0 below n collect
`(ash (read-byte stream)
,(* 8 (if be (- n 1 i) i)))))))
(defmacro make-read-s (n be)
`(defun ,(intern (format nil "READ~d~:[L~;B~]E-S" n be))
(&optional (stream *standard-input*))
(let ((a (,(intern (format nil "READ~d~:[L~;B~]E" n be)) stream)))
(if (zerop (logand a ,(ash 1 (1- (* 8 n)))))
a
(logior a ,(ash -1 (* 8 n)))))))
(defmacro make-write (n be)
`(defun ,(intern (format nil "WRITE~d~:[L~;B~]E" n be))
(n &optional (stream *standard-output*))
(setf n (logand n ,(1- (ash 1 (* 8 n)))))
,@(loop for i from 0 below n collect
`(write-byte (ldb (byte 8 ,(* 8 (if be (- n 1 i) i))) n)
stream))))
(eval-when (:compile-toplevel :load-toplevel :execute)
(dolist (cat '("READ" "READ-S" "WRITE"))
(dolist (be '(nil t))
(dolist (n '(1 2 4 8))
(eval `(,(intern (format nil "MAKE-~a" cat)) ,n ,be))))))
It works. It generates functions for reading and writing unsigned and signed integers in sizes of 1, 2, 4, and 8. SLIME understands it. But I wonder if there are better ways.
What's the best way to write a bunch of similar functions in Common Lisp?
There are some issues with this code, though the general approach to have macros generating functions is fine.
Naming
The macros should not be named
make-...
, because they are not functions which make something, but macros which define a function.Code generation
The
EVAL-WHEN ... EVAL
code is really bad and should not be used this way.The better way is to write macro which expands into a
progn
with the function definitions.If I wanted to use
EVAL
, then I would not need to write code generating macros, but simply code generating functions. But I don't want to useEVAL
, I want to create code for the compiler directly. If I have code generating macros, then I don't needEVAL
.EVAL
is not a good idea, because it is not clear that the code would be compiled - which would be implementation dependent. Also the evaluation would take place at compile time and load time. It would be better to compile the functions at compile time and only load them at load time. A file compiler also might miss possible optimizations for the evaluated functions.Instead of the
EVAL-WHEN ... EVAL
we define another macro and then we use it later:Now we can use above macro to generate all the functions:
You can see the expansion here:
Each of the subforms then will be expanded into the function definitions.
This way the compiler runs the macros to generate all the code at compile time and the compiler can then generate code for all the functions.
Efficiency / Defaults
In a lowest-level function I may not want to use an
&optional
parameter. The default call would get the value from a dynamic binding and, worse,*standard-input*
/*standard-output*
may not be a stream for whichREAD-BYTE
orWRITE-BYTE
works. Not in every implementation you can use a standard input/output stream as a binary stream.LispWorks:
I also may want to declare all generated functions to be inlined.
Type declarations would be another thing to think about.
Summmary: don't use EVAL.