如何在Common Lisp中编写类似的函数?

8

我正在学习 Common Lisp,这本书是从Practical Common Lisp中学到的。其中第24章有一些有关读写二进制文件的帮助函数的例子,以下是其中一个示例:

(defun read-u2 (in)
  (+ (* (read-byte in) 256) (read-byte in)))

我同样可以编写用于读取其他类型二进制数的函数。但我认为这样做违反了DRY原则。此外,这些函数可能会相似,因此我尝试使用宏来生成这些函数。

(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))))))

它能够工作。它生成用于读写1、2、4和8字节无符号和有符号整数的函数。SLIME理解它。但我想知道是否有更好的方法。

在Common Lisp中编写大量类似的函数,最佳方式是什么?

2个回答

9
虽然宏生成函数的一般方法是正确的,但这段代码存在一些问题。
命名:
宏不应该被命名为“make-...”,因为它们不是制造某些东西的函数,而是定义函数的宏。
代码生成:
EVAL-WHEN ... EVAL 代码非常糟糕,不应该这样使用。更好的方法是编写宏,该宏扩展为具有函数定义的progn。如果我想使用EVAL,那么我就不需要编写生成代码的宏,而是直接生成函数。但我不想使用EVAL,我想直接为编译器创建代码。如果我有生成代码的宏,那么我就不需要使用EVAL。
EVAL 不是一个好主意,因为不清楚代码是否会被编译 - 这将取决于实现。此外,评估将在编译时和加载时发生。最好在编译时编译函数,并仅在加载时加载它们。文件编译器也可能错过了对评估函数的可能优化。
(defmacro def-read-fun (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 def-read-s-fun (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 def-write-fun (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 ... EVAL,然后稍后再使用它:

(defmacro def-reader/writer-functions (cat-list be-list n-list)
  `(progn
     ,@(loop for cat in cat-list append
             (loop for be in be-list append
                   (loop for n in n-list
                         collect `(,(intern (format nil "DEF-~a-FUN" cat))
                                   ,n
                                   ,be))))))

现在我们可以使用上述宏来生成所有的函数:
(def-reader/writer-functions
 ("READ" "READ-S" "WRITE")
 (nil t)
 (1 2 4 8))

您可以在此处查看扩展:
CL-USER 173 > (pprint (macroexpand-1 '(def-reader/writer-functions
                                       ("READ" "READ-S" "WRITE")
                                       (nil t)
                                       (1 2 4 8))))

(PROGN
  (DEF-READ-FUN 1 NIL)
  (DEF-READ-FUN 2 NIL)
  (DEF-READ-FUN 4 NIL)
  (DEF-READ-FUN 8 NIL)
  (DEF-READ-FUN 1 T)
  (DEF-READ-FUN 2 T)
  (DEF-READ-FUN 4 T)
  (DEF-READ-FUN 8 T)
  (DEF-READ-S-FUN 1 NIL)
  (DEF-READ-S-FUN 2 NIL)
  (DEF-READ-S-FUN 4 NIL)
  (DEF-READ-S-FUN 8 NIL)
  (DEF-READ-S-FUN 1 T)
  (DEF-READ-S-FUN 2 T)
  (DEF-READ-S-FUN 4 T)
  (DEF-READ-S-FUN 8 T)
  (DEF-WRITE-FUN 1 NIL)
  (DEF-WRITE-FUN 2 NIL)
  (DEF-WRITE-FUN 4 NIL)
  (DEF-WRITE-FUN 8 NIL)
  (DEF-WRITE-FUN 1 T)
  (DEF-WRITE-FUN 2 T)
  (DEF-WRITE-FUN 4 T)
  (DEF-WRITE-FUN 8 T))

每个子表单都将扩展为函数定义。
这样编译器在编译时运行宏以生成所有代码,然后编译器可以为所有函数生成代码。
效率/默认值:
在最低级别的函数中,可能不想使用可选参数。默认调用将从动态绑定获取值,更糟糕的是,*standard-input* / *standard-output* 可能不是 READ-BYTE 或 WRITE-BYTE 可用的流。并非每个实现都可以将标准输入/输出流用作二进制流。
LispWorks:
CL-USER 1 > (write-byte 13 *standard-output*)

Error: STREAM:STREAM-WRITE-BYTE is not implemented for this stream type: #<SYSTEM::TERMINAL-STREAM 40E01D110B>
  1 (abort) Return to level 0.
  2 Restart top-level loop.

我也可能希望声明所有生成的函数都是内联的。
类型声明也是需要考虑的事情。
总结:不要使用EVAL。

为什么不使用&optional?如果出于效率的原因,如果函数是内联的,它是否仍然适用? - nisekgao

2
通常,我更喜欢将要读取的字节数作为另一个参数添加到函数中:
(defun read-integer (stream bytes)
  (check-type bytes (integer 1 *))
  (loop :repeat bytes
        :for b := (read-byte stream)
        :for n := b :then (+ (* n 256) b)
        :finally (return n)))

“Signedness”和“endianness”可以作为关键字参数添加。这种编程方式有助于编写易于理解且易于通过诸如SLIME等工具进行导航的代码。
通过宏展开来实现优化是一种有效的策略,我推荐参考Rainer's answer
对于从流中读取数字的特定情况,优化很可能是一个合理的目标,因为这通常在紧密循环中频繁使用。
然而,如果这样做,您还应该彻底记录生成的内容。如果代码的读者看到一个名为read8bes的运算符,他不容易找出它是在哪里定义的。您需要帮助他。

1
对于这样一个通用的函数,您还需要记录8位字节的假设...;-) - Rainer Joswig

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接