我怎样才能正确地制定我的 "format" 函数?

How can I CORRECTLY formulate my "format" function?

我有一个功能:

(defun play (&rest args)
  (format nil "play ~A~{, ~A~}~%" (car args) (cdr args))))

而且,据我所知,应该这样使用:

(play 1 2 3)

并且,在这种情况下 return“播放 1 2 3”。不幸的是,这段代码中有一些错误,所以我的 emacs 编辑器 return 是这样的:

main.lisp:7:7:
  error: 
    don't know how to dump #<SWANK/GRAY::SLIME-OUTPUT-STREAM {1004139673}> (default MAKE-LOAD-FORM method called).
    ==>
      #<SWANK/GRAY::SLIME-OUTPUT-STREAM {1004139673}>
    
  note: The first argument never returns a value.
  note: 
    deleting unreachable code
    ==>
      "play ~A~{, ~A~}~%"

你能帮我写这个函数吗?

你的例子也适用于我(虽然它是人为的,请参阅评论)。

但是,这里有一个使用外部库的例子,if/when你不记得格式指令了…

;; (ql:quickload "str")
(format t "play ~a" (str:join ", " (list 1 2 3)))
;; => play 1, 2, 3

错误

在你的代码中,你写:

(defun play (&rest args)
  (format #.*standard-output* "play ~A~{, ~A~}~%" (car args) (cdr args))))

在评估代码之前,首先阅读:源代码变成了抽象语法树,表示为 lisp 值。在遇到 #.*standard-output* 时,reader 评估下一个表单,这里 *standard-output*:当您 读取 表单存储时绑定到此变量的实际流structured-expression 中是函数 play.

的源代码

您可以在代码中放置任意数据,特别是字符串、数字等。此处您的源代码包含一个流实例。当您尝试执行函数 play 时,这可能是个问题,因为在您计算函数主体时,流可能已关闭;看起来您正在尝试修复要写入输出的流,但这可能不是执行您的想法的好方法。如果您编辑您的问题以添加更多相关信息,我们可能会找到更好的方法来解决您的问题。

编译代码时,存储流对象也是一个问题,这可能是您在此处出现此特定错误的原因。

如评论中所述,编译文件时会发生这种情况。如果您正在使用 Slime,当环境使用临时文件编译缓冲区时可能会发生这种情况。

所以你的错误发生在使用COMPILE-FILE时;请特别注意规范中的这一段:

Programs to be compiled by the file compiler must only contain externalizable objects; for details on such objects, see Section 3.2.4 (Literal Objects in Compiled Files). For information on how to extend the set of externalizable objects, see the function make-load-form and Section 3.2.4.4 (Additional Constraints on Externalizable Objects).

文件编译器生成目标文件(fasl 扩展名)。一些字面值,如常量字符串等,必须存储在结果文件中,以便在加载回文件 (§3.2.4 Literal Objects in Compiled Files) 时可以重建它们 相似的对象 。对于大多数标准类型,Lisp 编译器知道如何转储对象并将其加载回来。

但是这里编译器不知道如何在编译过程中转储stream类型的值。您可以为 MAKE-LOAD-FORM 定义方法,但有些数据本身很难(反)序列化:我们如何存储线程或任何其他操作系统资源?

格式

除了在 read-time 计算 *standard-output* 外,格式有效。

但是,您将列表分解为 carcdr,但是:

  1. 你没有检查列表中是否有任何元素;如果 args 为 NIL,则打印文本为 "play NIL",这与一个元素 NIL 的列表不明确;或者您可能只使用 non-empty 列表调用该函数,在这种情况下,您可能应该添加 (check-type list cons) 以更好地捕获编程错误。或者,定义 play 如下:

    (defun play (arg &rest more-args)
      ...)
    

    那么,ARG是car,MORE-ARGS是cdr,用户不能调用args为空列表的函数(约束在函数签名中也更明确).

  2. 你分解它们来处理元素之间的逗号添加,这是 format 已经可以自己处理的事情。

    (format t "~{~a~^, ~}" args)
    

    如果正在处理的列表中没有剩余元素,~^ 插入符运算符将以格式退出当前迭代上下文。所以在这里,我们只在列表中有更多元素时添加一个逗号和一个space。 此外,您可能希望处理空列表并打印其他内容,例如 not playing anything 以防列表为空;在这种情况下,您使用以下结构:

    "~:[  ...  ~;   ...  ~]"
    

    带冒号修饰符的括号运算符就像一个if,~;之前的部分是针对NIL的情况,~;之后的部分是针对non-nil的部分;你需要这样写:

    (format t "~:[not playing anything~;play ~{~a~^, ~}~]" args args)
    

    注意 args 是如何提供两次的:第一次用于测试,然后(仅)在测试的真实分支中使用它。我们可以通过让 format 在其参数列表上倒带来做得更好,这样它就可以使用单个参数 args 两次:

    (format t "~:[not playing anything~;~:*play ~{~a~^, ~}~]" args)
    

    带有冒号修饰符 ~:* 的星号运算符通过返回提供的参数列表中的一个来更改 format 使用的当前参数。

您可以在此处阅读有关格式的更多信息:一些格式食谱

正如其他人所指出的,这里的问题是 #.*standard-output*。我认为可能值得解释一下它的作用以及为什么它真的不起作用。

编译文件的过程包括如下内容:

  1. 从文件中读取表格;
  2. 将该表单转换为编译代码,其中可能包含对文字对象的各种引用;
  3. 将编译后的代码写入 fasl 文件。

您想要做的是,在 (1) 中,使 reader(通过 #.)在读取时 计算表达式 然后将其作为文字值连接到代码中。所以特别是在代码中结束的是不是对特定动态变量的引用,*standard-output*,而是该变量绑定到的流reader 阅读表格的那一刻 .

编译器 (2) 并不真正关心这一点:它很乐意编译其中包含任何类型文字的代码,真的。

但是在 (3) 处,系统必须将这个文字对象写出到一个名为 'externalizing' 的文件中。它必须以这样的方式进行,稍后,可能在不同机器上的不同 lisp 图像 运行 中,在加载文件时可以构造类似的对象。 'being similar' 的意思对于字符串、cons 或符号(以及其他一些类型)之类的东西有一个相当自然的定义。该规范在 3.2.4.

的小节中用了一些篇幅来定义相似性的含义以及哪些类型的对象必须是可外部化的

除了标准要求可作为文字外化的对象类型外,实现可以自由定义其他类型,并且可以通过在 make-load-form 上定义方法来定义自己的对象类型,尽管这仅对某些 类 对象具有可移植性。

然而,非常清楚的是,某些 类 对象在编译文件中显示为文字根本没有任何实际意义,因为没有对它们有意义的相似性的有用定义。

流就是其中之一 类。看看为什么要考虑像这样的代码

 (with-open-file (*standard-output*
                             "/my/directory/sdfghjk.out"
                             :if-exists :supersede)
              (compile-file "my-source-file.lisp"))

现在文件编译器遇到一段文本形式为

的源代码应该怎么办
(defun foo ()
  #.*standard-output*)

比如说? fasl 转储器看到的是一个文字对象,它是一个流,打开一个特定文件,在特定机器上的特定目录中。它应该向 fasl 文件写入什么,以便在稍后加载 fasl 文件时可以创建类似的流?构建一个类似的流到底意味着什么?如果加载 fasl 文件时该目录不存在,应该怎么办?

嗯,这个问题的答案是,一般来说,它根本没有任何意义:没有适用于流的普遍有用的相似性定义,因为就其本质而言,它们是有意义的对象在单个 运行 图像中。

再次注意:代码中的结尾是不是对变量的引用,这完全没问题,它是对对象的引用这是读取表单时该变量的值,这就是导致问题的原因。


最后一点:#. 是一件很不错的事情,但就像大多数美好的事情一样,它也有缺点。在这种情况下,缺点是 reader 可以在读取时执行任意代码 ,其中 'arbitrary' 表示 'anything at all'。这在编译代码时可能是合理的(如果您信任该代码的作者),但通常在读取数据时执行任意代码的能力有一个名字:代码注入攻击。因此,如果您使用 read 作为您不完全信任的读取数据的工具,请通过将 *read-eval* 绑定到 nil 关闭此功能