3

我在(常见的)lisp 中看到了两种不同的“输出”函数模式:

(defun implicit ()
  (format t "Life? Don't talk to me about life!"))

(defun explicit (stream)
  (format stream "This will all end in tears."))

(defun test-im-vs-ex-plicit ()
  (values
   (with-output-to-string (stream)
     (let ((*standard-output* stream))
       (implicit)))
   (with-output-to-string (stream)
     (explicit stream))))

使用动态范围是否像implicit被认为是不好的做法,或者这是一种普遍接受的动态范围使用?请注意,我假设这是为了例如 DSL 来构建复杂的输出,如 HTML、SVG、Latex 或其他任何东西,并且除了生成打印表示之外不会做任何不同的事情。

除了风格之外,是否存在任何重要的差异,例如在性能、并发性或其他方面?

4

4 回答 4

5

其实你可以*standard-output*直接绑定:

(defun test-im-vs-ex-plicit ()
  (values
   (with-output-to-string (*standard-output*)   ; here
     (implicit))
   (with-output-to-string (stream)
     (explicit stream))))

没有真正简单的答案。我的建议:

使用流变量,这使得调试更容易。它们出现在参数列表中,并且更容易在回溯中发现。否则,您需要在回溯中查看流变量的动态重新绑定。

a) 没有什么可以通过的?

(defun print-me (&optional (stream *standard-output*))
  ...)

b) 一个或多个固定参数:

(defun print-me-and-you (me you &optional (stream *standard-output*))
  ...)

c) 一个或多个固定参数和多个可选参数:

(defun print-me (me
                 &key
                 (style  *standard-style*)
                 (font   *standard-font*)
                 (stream *standard-output*))
  ...)

还要注意这一点:

现在假设(implicit)有一个错误,我们得到一个中断循环,一个调试repl。这个中断循环中标准输出的价值是什么?

CL-USER 4 > (defun test ()
              (flet ((implicit ()
                       (write-line "foo")
                       (cerror "go on" "just a break")
                       (write-line "bar")))
                (with-output-to-string (stream)
                  (let ((*standard-output* stream))
                    (implicit)))))
TEST

CL-USER 5 > (compile 'test)
TEST
NIL
NIL

CL-USER 6 > (test)

Error: just a break
  1 (continue) go on
  2 (abort) Return to level 0.
  3 Return to top loop level 0.

Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.

CL-USER 7 : 1 > *standard-output*
#<SYSTEM::STRING-OUTPUT-STREAM 40E06AD80B>

CL-USER 8 : 1 > (write-line "baz")
"baz"

CL-USER 9 : 1 > :c 1
"foo
baz
bar
"

以上是您在 LispWorks 或 SBCL 中看到的内容。在这里您可以访问真实程序的绑定,但在调试期间使用输出函数会对该流产生影响。

在其他实现*standard-output*中将反弹到实际终端 io - 例如在 Clozure CL 和 CLISP 中。

如果您的程序没有重新绑定*standard-output*,那么这些情况下的混乱就会减少。如果我编写代码,我经常会考虑在 REPL 环境中什么会更有用——这与语言不同,在 REPL 和中断循环上的交互式调试较少......

于 2015-05-31T11:26:43.317 回答
1

我不是 Lisp 专家,但我见过大量使用*standard-output*. lisp 社区的论点是这种方法使代码更容易在 REPL 中运行/测试(我来自 C/Java 背景,所以任何有全局变量气味的东西都会让人感到不安,但这是 lisp 方式)。

关于并发,CL 中的每个线程都有不同的副本*standard-output*,因此您的线程将是安全的,但您需要正确配置它们。您可以在lisp 食谱 - 线程部分了解更多相关信息。

于 2015-05-31T09:26:20.487 回答
1

我只是想补充一点,你可以在 Common Lisp 中做的一件事就是结合这两种做法:

(defun implicit (&optional (message "Life? Don't talk to me about life!"))
  (format t message))

(defun explicit (*standard-output*)
  (implicit "This will all end in tears."))

由于*standard-output*是参数的名称,因此explicit使用流参数调用会自动将动态变量重新绑定*standard-output*到该参数的值。

于 2015-05-31T17:43:35.247 回答
1

除了显式传递流之外,我在 Common Lisp 本身中看到的唯一与统计相关的模式是一个可选的流参数,它默认为*standard-input**standard-output*取决于函数所需的方向。

Common Lisp 中的隐式用例都处理未指定的输入/输出,例如:

  • y-or-n-p/yes-or-no-p使用哪个*query-io*

  • aproposdisassemble以及room使用哪个*standard-output*

  • describe可以使用*standard-output*或者*terminal-io*

  • trace/untracetime使用哪个*trace-output*

  • dribble这可能会绑定*standard-input*和/或*standard-output*

  • step并且inspect可以为所欲为,从无到有,到标准输入和标准输出命令循环,再到显示图形工具窗口

所以,我相信你可能见过的所有其他案例都来自图书馆。我的建议是不要遵循任何隐含的模式。然而,一个很好的例外是在 HTML 生成器中,它绑定了一些变量,例如*html-stream*,以便后面的宏可以引用该变量而不会造成混乱。想象一下,如果你必须告诉每个宏上的流(不是一个真实的例子):

(html
  (head (title "Foo"))
  (body (p "This is just a simple example.")
        (p "Even so, try to imagine this without an implicit variable.")))

对于实际示例,请查看(至少)CL-WHO (with-html-output)AllegroServe 的 HTML 生成器

所以,这里的优势是纯粹的语法。

使用动态绑定从来没有性能方面的原因。可能有堆栈空间的原因,以避免将流作为参数传递,但这是一个非常弱的原因,任何现有的递归只会进一步吹嘘。

于 2015-06-01T14:08:15.910 回答