如何正确制定我的“格式”功能?

时间:2020-11-01 15:31:23

标签: formatting common-lisp

我有一个功能:

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

据我所言,应该这样使用:

(play 1 2 3)

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

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~}~%"

您能帮我写这个功能吗?

3 个答案:

答案 0 :(得分:2)

错误

在您的代码中,您编写:

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

在对代码进行评估之前,必须先对其进行读取:将源代码转换成抽象语法树,并以lisp值表示。遇到#.*standard-output*时,读者将评估下一个表单,这里为*standard-output*:当您 read 将表单存储在结构化表达式中时,绑定到此变量的实际流将存储在函数play的源代码。

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

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

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

因此,使用COMPILE-FILE时会发生错误;特别注意规范中的这一段:

要由文件编译器编译的程序只能包含可外部化的对象;有关此类对象的详细信息,请参见第3.2.4节(编译文件中的文字对象)。有关如何扩展可外部化对象集的信息,请参见make-load-form函数和第3.2.4.4节(可外部化对象的附加约束)。

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

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

格式

除了在读取时评估*standard-output*之外,该格式均有效。

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

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

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

    然后,ARG是汽车,MORE-ARGS是cdr,并且用户无法使用空的args列表调用函数(该约束在函数签名中也更加明确)。

  2. 您可以分解它们以处理元素之间的逗号添加,format已经可以自己处理。

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

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

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

    带有冒号修饰符的方括号运算符类似于if,~;之前的部分用于NIL情况,~;之后的部分用于非nil部分;您将需要编写如下内容:

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

    请注意args是如何提供两次的:首先用于测试,然后(仅)在测试的真实分支中使用它。让格式倒退到其参数列表上,我们可以做得更好,这样它就可以一次使用一个参数args两次:

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

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

您可以在此处了解有关格式的更多信息:A Few FORMAT Recipes

答案 1 :(得分:2)

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

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

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

您想要做的是,在(1)中,使读者(通过#.)在读取时对表达式求值,然后将其作为代码连接到代码中字面值。因此,特别是代码中的结尾不是 对特定动态变量*standard-output*的引用,而是该变量在读者阅读时绑定的流表格

编译器(2)并不真正在乎这一点:确实很高兴编译其中包含任何文字的代码。

但是在(3),系统必须将此文字对象写到文件中,称为“外部化”。而且,它必须以某种方式执行此操作,以便以后可能在不同计算机上运行的另一个Lisp映像中,在加载文件时构造一个相似的对象。 “相似”的含义是对诸如字符串,利弊或符号(以及其他一些类型)之类的东西的自然定义。规范在一定程度上定义了相似性的含义以及在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 dumper看到的是一个文字对象,该对象是在特定计算机上特定目录中的特定文件打开的流。它应该向fasl文件写入什么内容,以便稍后加载fasl文件时可以创建类似的流?完全构建类似的流甚至意味着什么?如果在加载fasl文件时该目录不存在,应该怎么办?

好吧,对此的答案是,它根本没有任何意义:没有通用的相似性定义适用于流,因为从本质上说,它们是在一个对象中有意义的对象。单个正在运行的图像。

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


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

答案 2 :(得分:1)

您的示例也对我有用(尽管它是人为设计的,请参见注释)。

但是,如果/当您不记得格式指令时,这是使用外部库的示例……

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