【问题标题】:How to write similar functions in Common Lisp?如何在 Common Lisp 中编写类似的函数?
【发布时间】:2017-01-01 11:00:11
【问题描述】:

我正在向Practical Common Lisp 学习 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 中编写一堆类似函数的最佳方法是什么?

【问题讨论】:

    标签: common-lisp dry preprocessor


    【解决方案1】:

    虽然使用宏生成函数的一般方法很好,但这段代码存在一些问题。

    命名

    不应将宏命名为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.
    

    我可能还想声明所有生成的函数都被内联。

    类型声明是另一回事。

    总结:不要使用 EVAL。

    【讨论】:

    • 为什么不使用&amp;optional?如果是为了效率,如果函数是内联的,它是否仍然适用?
    【解决方案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,他就不能轻易找出它是在哪里定义的。你需要帮助他。

    【讨论】:

    • 对于这样一个通用函数,您还需要记录 8 位字节的假设... ;-)
    猜你喜欢
    • 2019-11-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-08
    • 1970-01-01
    • 2015-12-12
    • 2014-04-26
    • 2019-01-18
    相关资源
    最近更新 更多