【问题标题】:Define a constant array of struct known at compilation-time定义在编译时已知的结构常量数组
【发布时间】:2021-12-26 13:32:55
【问题描述】:

在我的程序中,我有常量字符串,这些值在编译时是已知的。对于每个偏移量,当前有 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)))

这似乎有效,因为它返回以下内容:

[test-2-1]
[test-2-2]

BLOCK-1 中,为compile-timeload-timeexecute-time 定义了包含数据的struct。在BLOCK-2 中,创建向量并设置值的代码在compile-time 处执行。

但我有以下顾虑:

  • 这段代码似乎没有必要冗长
  • 字符串存储在structure
  • 我需要手动设置每个值的offset(aref vector 0)(aref vector 1) 等)。
  • 当我在 BLOCK-1 中设置 ITEM-1 而不是 BLOCK-2 时,我在 SBCL 中收到一个我不明白的错误

Common Lisp 中定义复杂常量的惯用方式是什么?

【问题讨论】:

    标签: common-lisp


    【解决方案1】:

    从你的问题中并不清楚你想做什么。

    第一个重要提示:您的代码已严重损坏。它已损坏,因为您仅在编译时定义 +global-vector-constant+ ,但稍后再引用它。如果您编译此文件,然后将该编译后的文件加载到冷映像中,则会出现错误。

    在处理此类事情时绝对关键,以确保您的代码能够在冷的 Lisp 中编译。常驻环境的经典问题之一(与 Interlisp-D 的方式相比,CL 并不是真的)最终会导致无法冷构建的系统:我很确定我工作了好几个多年以来,没有人知道如何冷构建的 Interlisp-D 系统输出。

    如果你想要的是一个对象(例如一个数组),它的初始值是在编译时计算出来的,然后被当作一个字面量处理,那么答案通常是一个宏:宏就是函数在编译时完成它们的工作,因此宏可以扩展为文字。此外,您希望成为文字的对象必须是 externalizable (这意味着“可以转储到已编译文件中”)并且其中涉及的任何内容在编译时都是已知的。一些类的实例默认是可外部化的,另外一些类的实例可以通过用户代码实现外部化,还有一些根本不能外部化(例如实例函数)。

    在很多简单的情况下,比如你给出的那个,如果我理解的话,你并不真的需要一个宏,事实上你几乎总是可以不用宏,尽管它可能会使你的代码更容易了解您是否确实使用了一个。

    这是一个简单的例子:许多数组都是可外部化的,如果它们的元素是

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

    这会做同样的事情,但稍早一些(在读取时,而不是在宏扩展/编译时)。所以这真的一无所获。

    但你也可以这样做这个

    (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-&gt;svector 中,load-time-value 用于在加载时仅创建一次扫描仪,这是一个相关的技巧。

    【讨论】:

    • 非常感谢您的回答,我从中学到了一些东西。您能否编辑您的答案只是为宏用法添加一个示例。是否像(defmacro create-const-table () #(#s(foo :l "0-l" :r "0-r")))(defparameter table (create-const-table)) 一样直截了当?如果是这样,那我明白你的意思了,这似乎完全没有必要。
    • @Robert:我添加了一个宏可能有用的案例,这也演示了使用load-time-value 来执行类似的技巧:计算一些值,您稍后将使用但不要'不想在全局中或在每次调用时计算。
    • 谢谢@ignis volens,我喜欢你基于宏的例子,我觉得很清楚。
    【解决方案2】:

    首先,您的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

    【讨论】:

    • :ELEMENT-TYPE(也在原始问题中)没有意义?反正不会升级到T吗?你会把它留作文档吗?
    • @RainerJoswig:是的,这就是我最后一句话要说的——仅限文档。
    • 谢谢你们的回答,不幸的是它并没有真正帮助我,我已经知道结构、让和循环。我在实践中从未正确理解的实际上是eval-when。我正在寻找一个小而完整的示例,说明如何在编译时定义结构数组。恐怕我写的代码示例实际上不能正常工作,我今天试了一下,它似乎坏了。我不需要loop,因为我所有的常量都是手动设置的。我也不确切知道defconstant 是否可以在这里工作。
    • 我想说的另一点,这个问题不是一个阻塞点,因为我总是可以定义一个initialize 函数并在程序启动时调用它。但在这种情况下,表将在运行时构建。我想了解Common Lisp 是否有一些机制允许提前完成计算并向程序提供一个对象,其中包含先前初始化的表(C 世界中的一种静态数据,但来自 Lisp 代码的执行在编译时)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-06-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-06
    相关资源
    最近更新 更多