在下面let
,所有变量初始化表达式都具有完全相同的词法环境:包围着let
。如果这些表达式碰巧捕获了词法闭包,则它们都可以共享同一环境对象。
在下let*
,每个初始化表达式都在不同的环境中。对于每个连续的表达式,必须扩展环境以创建一个新的表达式。至少在抽象语义上,如果捕获了闭包,则它们具有不同的环境对象。
let*
必须对A进行充分优化,以消除不必要的环境扩展,以适合作为的日常替代let
。必须有一个编译器,该编译器的工作方式是访问哪些表单,然后将所有独立的表单转换为更大的,组合的let
。
(即使let*
只是发出级联let
形式的宏运算符,也是如此;优化是在级联的let
s上完成的)。
您不能使用隐藏变量赋值来实现let*
单个naivelet
来进行初始化,因为这样会显示出缺乏适当的作用域:
(let* ((a (+ 2 b))
(b (+ 3 a)))
forms)
如果变成
(let (a b)
(setf a (+ 2 b)
b (+ 3 a))
forms)
在这种情况下将不起作用;内部b
遮蔽了外部,b
所以我们最后将2加到上nil
。如果我们对所有这些变量进行alpha重命名,则可以完成这种转换。然后将环境很好地展平:
(let (#:g01 #:g02)
(setf #:g01 (+ 2 b)
#:g02 (+ 3 #:g01))
alpha-renamed-forms)
为此,我们需要考虑调试支持。如果程序员使用调试器进入此词法范围,我们是否希望他们处理#:g01
而不是a
。
因此,从根本上说,let*
是复杂的结构,必须对其进行优化以使其性能良好,并且let
在可能降低到的情况下也是如此let
。
这本身就不能证明有利于let
过let*
。假设我们有一个好的编译器;为什么不一直使用let*
?
作为一般原则,与易于出错的低级结构相比,我们应该偏向于使我们具有生产力并减少错误的高级别结构,并尽可能依赖于高级别结构的良好实现,因此我们几乎不必牺牲为了性能而使用它们。这就是为什么我们首先使用Lisp这样的语言进行工作的原因。
该推理不适用于let
vs let*
,因为相对于,let*
显然不是更高层次的抽象let
。他们是关于“平等水平”的。使用let*
,您可以引入只需切换到即可解决的错误let
。而反之亦然。let*
实际上,它只是用于视觉折叠let
嵌套的温和语法糖,而不是重要的新抽象。