lisp:动态范围与显式参数传递
lisp: dynamic scope vs explicit parameter passing
我在(通用)lisp 中看到 "output" 函数的两种不同模式:
(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 或其他任何东西,除了生成打印表示外,预计不会做任何不同的事情。
除了风格之外,是否存在任何重要差异,例如关于性能、并发性或其他什么?
我不是 Lisp 专家,但我见过很多代码使用 *standard-output*
的隐式值。 lisp 社区的论点是,这种方法使代码更容易 run/test 在 REPL 中(我来自 C/Java 背景,所以任何闻起来全局变量的东西都会让人感到不安,但它是 lisp方式)。
关于并发,CL中的每个线程都有不同的副本*standard-output*
,所以你的线程是安全的,但你需要适当地配置它们。您可以在 lisp cookbook - threads 部分阅读更多相关信息。
其实可以直接绑定*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)
有一个错误,我们得到一个 break loop,一个调试 repl。在这个break loop中standard-output的值是多少?
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 和中断循环上的交互式调试较少...
我只是想补充一点,您可以在 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*
重新绑定到该参数的值。
除了显式传递流之外,我在 Common Lisp 中看到的唯一与统计相关的模式是一个可选的流参数,它默认为 *standard-input*
或 *standard-output*
,具体取决于函数的方向需要。
Common Lisp中的隐式情况都是处理未指定的input/output,例如:
y-or-n-p
/yes-or-no-p
使用 *query-io*
apropos
、disassemble
和 room
使用 *standard-output*
describe
可以使用 *standard-output*
或 *terminal-io*
trace
/untrace
和 time
使用 *trace-output*
dribble
可能绑定 *standard-input*
and/or *standard-output*
step
和 inspect
可以为所欲为,从无到有,到标准输入和标准输出命令循环,再到显示图形工具 window
所以,我相信您可能看到的所有其他案例都来自图书馆。我的建议是不要遵循任何隐含的模式。然而,一个很好的例外是 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) and AllegroServe's HTML generator。
所以,这里的优势纯粹是语法上的。
从来没有使用动态绑定的性能原因。可能有一个堆栈 space 的原因,以避免将流作为参数传递,但这是一个非常弱的原因,任何现有的递归只会进一步吹一点。
我在(通用)lisp 中看到 "output" 函数的两种不同模式:
(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 或其他任何东西,除了生成打印表示外,预计不会做任何不同的事情。
除了风格之外,是否存在任何重要差异,例如关于性能、并发性或其他什么?
我不是 Lisp 专家,但我见过很多代码使用 *standard-output*
的隐式值。 lisp 社区的论点是,这种方法使代码更容易 run/test 在 REPL 中(我来自 C/Java 背景,所以任何闻起来全局变量的东西都会让人感到不安,但它是 lisp方式)。
关于并发,CL中的每个线程都有不同的副本*standard-output*
,所以你的线程是安全的,但你需要适当地配置它们。您可以在 lisp cookbook - threads 部分阅读更多相关信息。
其实可以直接绑定*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)
有一个错误,我们得到一个 break loop,一个调试 repl。在这个break loop中standard-output的值是多少?
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 和中断循环上的交互式调试较少...
我只是想补充一点,您可以在 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*
重新绑定到该参数的值。
除了显式传递流之外,我在 Common Lisp 中看到的唯一与统计相关的模式是一个可选的流参数,它默认为 *standard-input*
或 *standard-output*
,具体取决于函数的方向需要。
Common Lisp中的隐式情况都是处理未指定的input/output,例如:
y-or-n-p
/yes-or-no-p
使用*query-io*
apropos
、disassemble
和room
使用*standard-output*
describe
可以使用*standard-output*
或*terminal-io*
trace
/untrace
和time
使用*trace-output*
dribble
可能绑定*standard-input*
and/or*standard-output*
step
和inspect
可以为所欲为,从无到有,到标准输入和标准输出命令循环,再到显示图形工具 window
所以,我相信您可能看到的所有其他案例都来自图书馆。我的建议是不要遵循任何隐含的模式。然而,一个很好的例外是 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) and AllegroServe's HTML generator。
所以,这里的优势纯粹是语法上的。
从来没有使用动态绑定的性能原因。可能有一个堆栈 space 的原因,以避免将流作为参数传递,但这是一个非常弱的原因,任何现有的递归只会进一步吹一点。