Design patterns lisp:动态范围与显式参数传递
我在(通用)lisp中看到两种不同的“输出”函数模式: 像Design patterns lisp:动态范围与显式参数传递,design-patterns,coding-style,common-lisp,anti-patterns,dynamic-scope,Design Patterns,Coding Style,Common Lisp,Anti Patterns,Dynamic Scope,我在(通用)lisp中看到两种不同的“输出”函数模式: 像implicit中那样使用动态范围被认为是不好的做法,还是这是一种普遍接受的动态范围的使用?请注意,我假设这是为DSL构建复杂的输出,如HTML、SVG、Latex等,并且除了生成打印的表示之外,不需要做任何不同的事情 除了风格之外,还有什么重要的区别吗,例如在性能、并发性或其他方面?我不是Lisp专家,但我见过很多代码使用隐式值作为*标准输出*。lisp社区的观点是,这种方法使代码更容易在REPL中运行/测试(我来自C/Java背景,因
implicit
中那样使用动态范围被认为是不好的做法,还是这是一种普遍接受的动态范围的使用?请注意,我假设这是为DSL构建复杂的输出,如HTML、SVG、Latex等,并且除了生成打印的表示之外,不需要做任何不同的事情
除了风格之外,还有什么重要的区别吗,例如在性能、并发性或其他方面?我不是Lisp专家,但我见过很多代码使用隐式值作为
*标准输出*
。lisp社区的观点是,这种方法使代码更容易在REPL中运行/测试(我来自C/Java背景,因此任何带有全局变量味道的东西都会让人感到不安,但这是lisp方法)
关于并发性,CL中的每个线程都有一个不同的
*标准输出*
,因此线程是安全的,但您需要正确配置它们。您可以在本节中进一步了解这一点。实际上,您可以直接绑定*标准输出*
:
(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*))
...)
请注意:
现在假设(隐式)
有一个错误,我们得到一个中断循环,一个调试应答。这个中断循环中的标准输出值是多少
CL-USER 4>(除雾测试()
(flet((隐式()
(写行“foo”)
(cerror“继续”“休息一下”)
(写行“bar”))
(输出为字符串(流)
(let((*标准输出*流))
(隐含的((())))
试验
CL-USER 5>(编译“测试”)
试验
无
无
CL-USER 6>(测试)
错误:只是休息一下
1(继续)继续
2(中止)返回到级别0。
3返回顶部循环级别0。
键入:b表示回溯,或键入:c表示继续。
键入:错误报告模板的错误表单“”,或:?其他选择。
CL-USER 7:1>*标准输出*
#
CL-USER 8:1>(写行“baz”)
“巴兹”
CL-USER 9:1>:C1
“福
巴兹
酒吧
"
以上是您在LispWorks或SBCL中看到的内容。在这里,您可以访问实际程序的绑定,但在调试期间使用输出函数将对该流产生影响
在其他实现中,*标准输出*
将恢复到实际的终端io-例如在Clozure CL和CLISP中
如果您的程序没有重新绑定
*标准输出*
,那么在这些情况下就不会有太多混淆。如果我写代码,我经常会想,在REPL环境中什么更有用?这与语言不同,在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."))
由于
*标准输出*
是参数的名称,因此使用流参数调用显式
会自动将动态变量*标准输出*
重新绑定到该参数的值。除了显式传递流之外,我在Common Lisp本身看到的唯一统计相关模式,是一个可选的流参数,默认为*标准输入*
或*标准输出*
,具体取决于函数所需的方向
Common Lisp中的隐式用例都处理未指定的输入/输出,例如:
- 使用
*查询io*
,apropos
和反汇编
使用房间
*标准输出*
描述可以使用
或*标准输出*
*终端io*
/跟踪
和取消跟踪
使用时间
*跟踪输出*
可以绑定运球
和/或*标准输入*
*标准输出*
和step
可以做任何他们想做的事情,从零到标准输入和标准输出命令循环,再到显示图形工具窗口inspect
*HTML stream*
,这样后面的宏就可以引用该变量而不会造成混乱。想象一下,如果必须告诉每个宏上的流(不是真实的示例):
对于真实的例子,请查看(至少)和
所以,这里的优势是纯粹的合成
使用动态绑定从来没有性能原因。可能有堆栈空间的原因,以避免将流作为参数传递,但这是一个非常弱的原因,任何存在的递归都会进一步扩展。在REPL中进行更简单的测试确实是一个参数。我也有C/Java背景,这就是为什么我担心隐式使用是不好的做法。动态范围感觉有点像类固醇上的全局变量。感谢您提供有关线程的指针!是的,这是参数列表中特殊变量的结果。我不记得我在实际代码中看到过,但是……对于给定的数据块,如果它是“上下文”,并且每次调用都不会有太大变化,那么它可能是一个动态变量。如果它是“特定的”,并且可能会随着每次通话而改变
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
"
(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."))
(html
(head (title "Foo"))
(body (p "This is just a simple example.")
(p "Even so, try to imagine this without an implicit variable.")))