定义一个在编译时已知的常量结构数组
Define a constant array of struct known at compilation-time
在我的程序中,我有常量字符串,这些值在编译时已知。对于每个偏移量,当前有 2 个关联的字符串。我先写了下面的代码:
(eval-when (:compile-toplevel :load-toplevel :execute) ;; BLOCK-1
(defstruct test-struct
str-1
str-2))
(eval-when (:compile-toplevel) ;; BLOCK-2
(defparameter +GLOBAL-VECTOR-CONSTANT+ nil) ;; ITEM-1
(let ((vector (make-array 10
:initial-element (make-test-struct)
:element-type 'test-struct)))
(setf (test-struct-str-1 (aref vector 0)) "test-0-1")
(setf (test-struct-str-2 (aref vector 0)) "test-0-2")
(setf +GLOBAL-VECTOR-CONSTANT+ vector)))
(format t "[~A]~%" (test-struct-str-1 (elt +GLOBAL-VECTOR-CONSTANT+ 0)))
(format t "[~A]~%" (test-struct-str-2 (elt +GLOBAL-VECTOR-CONSTANT+ 0)))
这似乎有效,因为它 returns 以下内容:
[test-2-1]
[test-2-2]
在 BLOCK-1
中定义了包含数据的 struct
,用于 compile-time
、load-time
和 execute-time
。在 BLOCK-2
中,创建向量并设置值的代码在 compile-time
.
处执行
但我有以下顾虑:
- 这段代码似乎没有必要冗长
- 字符串存储在
structure
- 我需要手动设置每个值的
offset
((aref vector 0)
、(aref vector 1)
等)。
- 当我在
BLOCK-1
而不是 BLOCK-2
中设置 ITEM-1
时,我在 SBCL
中收到一个我不理解的错误
在 Common Lisp
中定义复常量的惯用方法是什么?
首先,你的let
代码可以简化为
(defparameter +global-vector-constant+
(let ((vector ...))
...
vector))
其次,你也可以做到
(defparameter +global-vector-constant+
(make-array 10 :element-type 'test-struct :initial-content
(cons (make-test-struct :str-1 "test-0-1" :str-2 "test-0-2")
(loop :repeat 9 :collect (make-test-struct)))))
请注意,:element-type 'test-struct
的好处通常仅限于代码自文档(参见 upgraded-array-element-type
)
你的问题并不清楚你想做什么。
第一个重要说明:您的代码已严重损坏。它已损坏,因为您仅在编译时定义 +global-vector-constant+
但稍后会引用它。如果编译此文件,然后将编译后的文件加载到冷映像中,则会出现错误。
在处理此类事情时绝对重要,以确保您的代码将在冷 Lisp 中编译。驻留环境的一个经典问题(与 Interlisp-D 的方式相比,CL 并不是真正的问题)是最终得到一个你不能冷构建的系统:我很确定我为几个人工作过多年使用 Interlisp-D 系统,没有人知道如何冷构建。
如果您想要的是一个对象(例如数组),其初始值在编译时计算,然后作为文字处理,那么一般来说,答案就是宏:宏正是在编译时完成工作的函数,因此宏可以扩展为文字。此外,您想要成为文字的对象必须是 externalizable(这意味着 'can be dumped in compiled files')并且其中涉及的任何内容在编译时都是已知的。某些 类 的实例默认是可外部化的,其他一些 类 的实例可以通过用户代码实现外部化,而有些则根本不可外部化(例如函数)。
在很多简单的情况下,比如你给出的那个,如果我理解的话,你并不真的需要一个宏,事实上你几乎总是可以不用宏,尽管它可能会让你的如果你使用一个代码更容易理解。
这是一个简单的例子:如果数组的元素是
,那么许多数组都是可外部化的
(defparameter *my-strings*
#(("0-l" . "0-r")
("1-l" . "1-r")))
这意味着 *my-strings*
将绑定到一个由字符串组成的文字数组。
一个更有趣的情况是元素是,例如结构。好吧,结构也是可外部化的,所以我们可以做到这一点。事实上,仍然很有可能避免使用宏,尽管它现在变得有点嘈杂。
(eval-when (:compile-toplevel :load-toplevel :execute)
(defstruct foo
l
r))
(defparameter *my-strings*
#(#s(foo :l "0-l" :r "0-r")
#s(foo :l "1-l" :r "1-r")))
请注意,以下 不会 起作用:
(defstruct foo
l
r)
(defparameter *my-strings*
#(#s(foo :l "0-l" :r "0-r")
#s(foo :l "1-l" :r "1-r")))
它不会工作,因为在编译时,你正试图外部化一个尚未定义的结构的实例(但如果 Lisp 是不冷,你甚至可以重新加载你用这种方式编译的文件)。同样,在这种情况下,您可以通过确保在加载具有 defparameter
的文件之前编译和加载定义 foo
结构的文件来避免在更大的系统中使用 eval-when
。
即使在更复杂的情况下,您也可以使用宏进行转义。例如,对于许多通常不可外部化的对象,您可以教系统如何将它们外部化,然后使用 #.
:
将对象拼接为文字
(eval-when (:compile-toplevel :load-toplevel :execute)
;; Again, this would be in its own file in a bigger system
(defclass string-table-wrapper ()
((strings)
(nstrings :initform 0)))
(defmethod initialize-instance :after ((w string-table-wrapper)
&key (strings '()))
(let ((l (length strings)))
(when l
(with-slots ((s strings) (n nstrings)) w
(setf s (make-array l :initial-contents strings)
n l)))))
(defmethod make-load-form ((w string-table-wrapper) &optional environment)
(make-load-form-saving-slots w :slot-names '(strings nstrings)
:environment environment))
) ;eval-when
(defgeneric get-string (from n)
(:method ((from string-table-wrapper) (n fixnum))
(with-slots (strings nstrings) from
(assert (< -1 n nstrings )
(n)
"bad index")
(aref strings n))))
(defparameter *my-strings*
#.(make-instance 'string-table-wrapper
:strings '("foo" "bar")))
请注意,当然,尽管 *my-strings*
的值是文字,但代码 运行 会在加载时重建此对象。但情况总是如此:只是在这种情况下,您必须定义 运行 所需的代码。除了使用 make-load-form-saving-slots
你可以自己做,例如通过这样的事情:
(defmethod make-load-form ((w string-table-wrapper) &optional environment)
(declare (ignore environment))
(if (slot-boundp w 'strings)
(values
`(make-instance ',(class-of w))
`(setf (slot-value ,w 'strings)
',(slot-value w 'strings)
(slot-value ,w 'nstrings)
,(slot-value w 'nstrtrings)))
`(make-instance ',(class-of w))))
但是 make-load-form-saving-slots
就容易多了。
这是一个示例,其中宏可能至少使阅读代码更容易。
假设您有一个从文件中读取字符串数组的函数,例如:
(defun file-lines->svector (file)
;; Needs CL-PPCRE
(with-open-file (in file)
(loop
with ltw = (load-time-value
(create-scanner '(:alternation
(:sequence
:start-anchor
(:greedy-repetition 1 nil
:whitespace-char-class))
(:sequence
(:greedy-repetition 1 nil
:whitespace-char-class)
:end-anchor)))
t)
for nlines upfrom 0
for line = (read-line in nil)
while line
collect (regex-replace-all ltw line "") into lines
finally (return (make-array nlines :initial-contents lines)))))
那么,如果这个函数在宏展开时可用,你可以写这个宏:
(defmacro file-strings-literal (file)
(check-type file (or string pathname) "pathname designator")
(file-lines->svector file))
现在我们可以创建字符串的文字向量:
(defparameter *fl* (file-strings-literal "/tmp/x"))
但是你完全可以这样做:
(defparameter *fl* #.(file-lines->svector "/tmp/x"))
它会做同样的事情,但会稍早一些(在阅读时,而不是在 macroexpansion/compile 时)。所以这真的是一无所获。
但你也可以这样做这个:
(defmacro define-stringtable (name file &optional (doc nil docp))
`(defparameter ,name ,(file-lines->svector file)
,@(if docp (list doc) nil)))
现在你的代码看起来像
(define-stringtable *st* "my-stringtable.dat")
这实际上是一个显着的改进。
最后请注意,在 file-lines->svector
中,load-time-value
用于在加载时仅创建一次扫描器,这是一个相关的技巧。
在我的程序中,我有常量字符串,这些值在编译时已知。对于每个偏移量,当前有 2 个关联的字符串。我先写了下面的代码:
(eval-when (:compile-toplevel :load-toplevel :execute) ;; BLOCK-1
(defstruct test-struct
str-1
str-2))
(eval-when (:compile-toplevel) ;; BLOCK-2
(defparameter +GLOBAL-VECTOR-CONSTANT+ nil) ;; ITEM-1
(let ((vector (make-array 10
:initial-element (make-test-struct)
:element-type 'test-struct)))
(setf (test-struct-str-1 (aref vector 0)) "test-0-1")
(setf (test-struct-str-2 (aref vector 0)) "test-0-2")
(setf +GLOBAL-VECTOR-CONSTANT+ vector)))
(format t "[~A]~%" (test-struct-str-1 (elt +GLOBAL-VECTOR-CONSTANT+ 0)))
(format t "[~A]~%" (test-struct-str-2 (elt +GLOBAL-VECTOR-CONSTANT+ 0)))
这似乎有效,因为它 returns 以下内容:
[test-2-1]
[test-2-2]
在 BLOCK-1
中定义了包含数据的 struct
,用于 compile-time
、load-time
和 execute-time
。在 BLOCK-2
中,创建向量并设置值的代码在 compile-time
.
但我有以下顾虑:
- 这段代码似乎没有必要冗长
- 字符串存储在
structure
- 我需要手动设置每个值的
offset
((aref vector 0)
、(aref vector 1)
等)。 - 当我在
BLOCK-1
而不是BLOCK-2
中设置ITEM-1
时,我在SBCL
中收到一个我不理解的错误
在 Common Lisp
中定义复常量的惯用方法是什么?
首先,你的let
代码可以简化为
(defparameter +global-vector-constant+
(let ((vector ...))
...
vector))
其次,你也可以做到
(defparameter +global-vector-constant+
(make-array 10 :element-type 'test-struct :initial-content
(cons (make-test-struct :str-1 "test-0-1" :str-2 "test-0-2")
(loop :repeat 9 :collect (make-test-struct)))))
请注意,:element-type 'test-struct
的好处通常仅限于代码自文档(参见 upgraded-array-element-type
)
你的问题并不清楚你想做什么。
第一个重要说明:您的代码已严重损坏。它已损坏,因为您仅在编译时定义 +global-vector-constant+
但稍后会引用它。如果编译此文件,然后将编译后的文件加载到冷映像中,则会出现错误。
在处理此类事情时绝对重要,以确保您的代码将在冷 Lisp 中编译。驻留环境的一个经典问题(与 Interlisp-D 的方式相比,CL 并不是真正的问题)是最终得到一个你不能冷构建的系统:我很确定我为几个人工作过多年使用 Interlisp-D 系统,没有人知道如何冷构建。
如果您想要的是一个对象(例如数组),其初始值在编译时计算,然后作为文字处理,那么一般来说,答案就是宏:宏正是在编译时完成工作的函数,因此宏可以扩展为文字。此外,您想要成为文字的对象必须是 externalizable(这意味着 'can be dumped in compiled files')并且其中涉及的任何内容在编译时都是已知的。某些 类 的实例默认是可外部化的,其他一些 类 的实例可以通过用户代码实现外部化,而有些则根本不可外部化(例如函数)。
在很多简单的情况下,比如你给出的那个,如果我理解的话,你并不真的需要一个宏,事实上你几乎总是可以不用宏,尽管它可能会让你的如果你使用一个代码更容易理解。
这是一个简单的例子:如果数组的元素是
,那么许多数组都是可外部化的(defparameter *my-strings*
#(("0-l" . "0-r")
("1-l" . "1-r")))
这意味着 *my-strings*
将绑定到一个由字符串组成的文字数组。
一个更有趣的情况是元素是,例如结构。好吧,结构也是可外部化的,所以我们可以做到这一点。事实上,仍然很有可能避免使用宏,尽管它现在变得有点嘈杂。
(eval-when (:compile-toplevel :load-toplevel :execute)
(defstruct foo
l
r))
(defparameter *my-strings*
#(#s(foo :l "0-l" :r "0-r")
#s(foo :l "1-l" :r "1-r")))
请注意,以下 不会 起作用:
(defstruct foo
l
r)
(defparameter *my-strings*
#(#s(foo :l "0-l" :r "0-r")
#s(foo :l "1-l" :r "1-r")))
它不会工作,因为在编译时,你正试图外部化一个尚未定义的结构的实例(但如果 Lisp 是不冷,你甚至可以重新加载你用这种方式编译的文件)。同样,在这种情况下,您可以通过确保在加载具有 defparameter
的文件之前编译和加载定义 foo
结构的文件来避免在更大的系统中使用 eval-when
。
即使在更复杂的情况下,您也可以使用宏进行转义。例如,对于许多通常不可外部化的对象,您可以教系统如何将它们外部化,然后使用 #.
:
(eval-when (:compile-toplevel :load-toplevel :execute)
;; Again, this would be in its own file in a bigger system
(defclass string-table-wrapper ()
((strings)
(nstrings :initform 0)))
(defmethod initialize-instance :after ((w string-table-wrapper)
&key (strings '()))
(let ((l (length strings)))
(when l
(with-slots ((s strings) (n nstrings)) w
(setf s (make-array l :initial-contents strings)
n l)))))
(defmethod make-load-form ((w string-table-wrapper) &optional environment)
(make-load-form-saving-slots w :slot-names '(strings nstrings)
:environment environment))
) ;eval-when
(defgeneric get-string (from n)
(:method ((from string-table-wrapper) (n fixnum))
(with-slots (strings nstrings) from
(assert (< -1 n nstrings )
(n)
"bad index")
(aref strings n))))
(defparameter *my-strings*
#.(make-instance 'string-table-wrapper
:strings '("foo" "bar")))
请注意,当然,尽管 *my-strings*
的值是文字,但代码 运行 会在加载时重建此对象。但情况总是如此:只是在这种情况下,您必须定义 运行 所需的代码。除了使用 make-load-form-saving-slots
你可以自己做,例如通过这样的事情:
(defmethod make-load-form ((w string-table-wrapper) &optional environment)
(declare (ignore environment))
(if (slot-boundp w 'strings)
(values
`(make-instance ',(class-of w))
`(setf (slot-value ,w 'strings)
',(slot-value w 'strings)
(slot-value ,w 'nstrings)
,(slot-value w 'nstrtrings)))
`(make-instance ',(class-of w))))
但是 make-load-form-saving-slots
就容易多了。
这是一个示例,其中宏可能至少使阅读代码更容易。
假设您有一个从文件中读取字符串数组的函数,例如:
(defun file-lines->svector (file)
;; Needs CL-PPCRE
(with-open-file (in file)
(loop
with ltw = (load-time-value
(create-scanner '(:alternation
(:sequence
:start-anchor
(:greedy-repetition 1 nil
:whitespace-char-class))
(:sequence
(:greedy-repetition 1 nil
:whitespace-char-class)
:end-anchor)))
t)
for nlines upfrom 0
for line = (read-line in nil)
while line
collect (regex-replace-all ltw line "") into lines
finally (return (make-array nlines :initial-contents lines)))))
那么,如果这个函数在宏展开时可用,你可以写这个宏:
(defmacro file-strings-literal (file)
(check-type file (or string pathname) "pathname designator")
(file-lines->svector file))
现在我们可以创建字符串的文字向量:
(defparameter *fl* (file-strings-literal "/tmp/x"))
但是你完全可以这样做:
(defparameter *fl* #.(file-lines->svector "/tmp/x"))
它会做同样的事情,但会稍早一些(在阅读时,而不是在 macroexpansion/compile 时)。所以这真的是一无所获。
但你也可以这样做这个:
(defmacro define-stringtable (name file &optional (doc nil docp))
`(defparameter ,name ,(file-lines->svector file)
,@(if docp (list doc) nil)))
现在你的代码看起来像
(define-stringtable *st* "my-stringtable.dat")
这实际上是一个显着的改进。
最后请注意,在 file-lines->svector
中,load-time-value
用于在加载时仅创建一次扫描器,这是一个相关的技巧。