2017-01-01 67 views
8

我正在学习从Practical Common Lisp的Common Lisp。它具有读取和第24章写二进制文件下面是一个例子辅助函数的例子:如何在Common Lisp中编写类似的函数?

(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中编写一堆类似函数的最好方法是什么?

回答

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

然后,每个子表单将被扩展到函数定义中。

这样编译器运行宏以在编译时生成所有代码,然后编译器可以为所有函数生成代码。

效率/默认

在最低级别的功能,我可能不希望使用&optional参数。默认调用将从动态绑定中获得值,更糟的是,*standard-input*/*standard-output*可能不是READ-BYTEWRITE-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. 

我可能还需要声明所有生成的函数内联。

类型声明是另一回事。

Summmary:不使用EVAL。

+0

为什么不使用'&optional'?如果是为了提高效率,如果功能是内联的,它是否仍然适用? – nisekgao

+0

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

的符号性和字节顺序,可以添加为关键字参数。这种编程方式对于易于理解的代码非常有用,这些代码也可以通过SLIME等工具轻松导航。

通过宏展开这是一个有效的优化策略,我遵循Rainer's answer

在从流中读取数字的特定情况下,从一开始就优化可能是一个有效的目标,因为这往往会在紧密循环中被大量使用。

但是,如果你这样做,你还应该彻底记录生成的内容。如果代码的读者看到一个运营商read8bes,他不能轻易找出它的定义。你需要帮助他。

+1

对于这样的一般功能,您还需要记录8位字节的假设...... ;-) –