如何在 Common Lisp 中编写类似的函数?
How to write similar functions in Common Lisp?
我正在从 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 编写一堆类似函数的最佳方法是什么?
此代码存在一些问题,但使用宏生成函数的一般方法很好。
命名
宏不应该命名为 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-BYTE
或 WRITE-BYTE
适用的流。并非在每个实现中都可以使用标准 input/output 流作为二进制流。
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。
一般来说,我更愿意将要读取的字节数作为另一个参数添加到函数中:
(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 等工具轻松导航。
通过宏展开这个是一个有效的优化策略,我遵从 。
在从流中读取数字的特定情况下,优化可能从一开始就是一个有效的目标,因为这往往会在紧密循环中大量使用。
但是,如果您这样做,您还应该彻底记录生成的内容。如果代码的 reader 看到运算符 read8bes
,他将无法轻易找到它的定义位置。你需要帮助他。
我正在从 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 编写一堆类似函数的最佳方法是什么?
此代码存在一些问题,但使用宏生成函数的一般方法很好。
命名
宏不应该命名为 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-BYTE
或 WRITE-BYTE
适用的流。并非在每个实现中都可以使用标准 input/output 流作为二进制流。
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。
一般来说,我更愿意将要读取的字节数作为另一个参数添加到函数中:
(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 等工具轻松导航。
通过宏展开这个是一个有效的优化策略,我遵从
在从流中读取数字的特定情况下,优化可能从一开始就是一个有效的目标,因为这往往会在紧密循环中大量使用。
但是,如果您这样做,您还应该彻底记录生成的内容。如果代码的 reader 看到运算符 read8bes
,他将无法轻易找到它的定义位置。你需要帮助他。