我一直在思考如何在不提供显式 goto 的 Lisp 中实现 Common Lisp 的
tagbody
,但提供 labels
。
我对此感兴趣的原因是我想编写一个递归解释器,支持 goto 语句,其工作方式与 common lisp 中完全相同。
假设有
labels
可用,是否可以将 tagbody
实现为用 labels
编写的宏?
考虑这个用
tagbody
编写的示例:
(tagbody
(go middle)
start
(print "start")
(go end)
middle
(print "middle")
(go start)
end
(print "end"))
现在考虑用
labels
编写的另一个示例:
(labels
((entry-point () (middle))
(start () (print "start") (end))
(middle () (print "middle") (start))
(end () (print "end")))
(entry-point))
表面上他们似乎做着同样的事情。有谁知道他们是否真的这样做?它们在语义上真的 100% 等效吗?
如果有人编写了一个将一种形式转换为另一种形式的宏,是否会出现一些边缘情况,其中细微的差异可能会重新出现?例如相同输入的不同输出,或微妙的变量范围差异,或堆栈展开的差异等。
如果您假设尾部调用变成 GO TO(这意味着您的代码将不可移植 CL),那么这在理论上是可能的,但在实践中可能非常困难。
实践中困难的原因是你需要处理动态状态,这将变成一场噩梦。例如考虑
(setf *x* 0)
(tagbody
start
(if (> *x* 0)
(go end)
(let ((*x* 1))
(go start)))
end)
其中
*x*
是一个特殊变量。这无法终止。但是
(setf *x* 0)
(labels ((start ()
(if (> *x* 0)
(end)
(let ((*x* 1))
(start))))
(end ()))
(start))
将在相同的假设下终止。
语言中还有许多其他动态状态,例如 catch 标记、异常处理程序以及您需要处理的许多其他内容。
从另一个角度思考这个问题会更容易:给定一个看起来像尾调用的函数的调用,您需要知道动态状态是什么,才能知道它是否真的是尾调用。
go
本质上迫使“调用”成为尾部调用,因此必须愿意解除在go
与其目标之间建立的任何动态状态。 (我认为在 CL 中,在这种情况下你永远不需要安装动态状态位。)