定义一个在编译时已知的常量结构数组

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-timeload-timeexecute-time。在 BLOCK-2 中,创建向量并设置值的代码在 compile-time.

处执行

但我有以下顾虑:

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 用于在加载时仅创建一次扫描器,这是一个相关的技巧。