let
和之间肯定存在效率争论let*
。但我们拥有的主要原因let
是历史性的,因为与lambda
.
let
在代码遍历 Lisp 解释器中实现更容易、更简单、更高效。如果环境有一些半体面的数据结构,而不仅仅是一个assoc
列表,则尤其如此。
假设解释器将环境实现为对象链。所以 for istance(let (a b) (let (c d) (let (e f))))
会将三个环境节点添加到环境链中。这些新节点中的每一个都包含两个绑定(在单独的列表或哈希表中)。
当我们解释let
表单时,我们可以评估传入环境链中的所有初始化表达式。我们可以在单个操作中为所有新绑定创建单个环境节点,并使用值填充绑定。
当我们解释let*
表格时,我们不能这样做。对于 中提到的每个连续绑定let*
,我们必须调用make-environment
、填充它并将其添加到环境链中,以便我们随后在扩展环境中解释下一个初始化形式。
这导致退化的运行时环境结构。虽然(let (a b c d ...))
生成了一个包含漂亮哈希表的环境对象,但(let* (a b c d ...))
生成了一个效率低下的链,需要 O(n) 遍历才能找到绑定。
let
我们可以消除和的解释器性能之间的差异,但只能通过将let*
性能拖到。如果我们将环境链表示为一个简单的列表,那么这个问题就无关紧要了;所有变量查找都是线性搜索。事实上,这样更容易实现:评估每个 init 表达式,并将新绑定推送到当前环境。let
let*
assoc
let*
现在,在图片中输入编译。 Lisp 编译器let*
只需对let
. 为了编译let*
,我们可以为所有绑定分配一个单一的环境(这会导致解释器中的范围不正确)。我们将该环境留空,并将其添加到编译时环境链中。因此,我们在该新环境的范围内编译 init 表达式。当我们遍历 init 表达式来编译它们时,我们将每个对应的变量一一添加到该环境中,以便后续 init 表达式的编译将在范围内具有该变量。
let*
是一个简单的 hack,当你有一个处理let
.
Lisp 编译器很容易生成环境的有效表示,而不管范围规则如何,解释器不一定如此。
由于口译员首先出现,这就解释了为什么let
是并行的。至少部分。另一个原因是它let
被实现为语法糖 over lambda
。但是lambda
(最初)在它自己的范围内根本没有初始化表达式;它只是指定变量。该lambda
表达式生成一个运行时对象,以便在调用函数时在运行时将值绑定到参数。参数表达式的评估在完全不同的范围内。
现在在立即调用lambda
中,这仍然是正确的:表达式的范围完全在 之外lambda
:
((lambda (a b) (+ a b)) 1 2)
表达式1
与;2
无关 lambda
它们没有包含在其中。
所以很明显,如果我们想要一个let
与上述相对应的糖表示法,我们必须小心地保留这个属性:
(let ((a 1) (b 2))
(+ a b))
如果我们想让 thislet
和之前的一样lambda
,我们必须让它看起来好像a
andb
是函数参数,1
and2
是参数表达式。
如果您是一名研究人员,正在使用一种有lambda
和没有s 的语言,并且let
渴望一种更好的方式来编写立即称为lambda
s 的语言,那么您不太可能发明let*
绑定语义。您将发明一些对您在整个代码中使用的现有结构具有清晰翻译策略的东西,以便您可以重构代码以毫无意外地使用它。
请注意,lambda
像 Common Lisp 这样的现代方言中确实嵌入了表达式:即可选参数和关键字参数!
(lambda (a &optional (b x) (c y) ...))
这些默认值表达式在周围的词法范围x
内y
进行评估,无论何时缺少参数,每次调用函数时。那么,这些表达式使用什么范围界定规则呢?为什么,串行,而不是并行!
[1]> (defun foo (x &optional (y (1+ x)) (z (1+ y)))
(list x y z))
FOO
[2]> (foo 10)
(10 11 12)
于是,事情就这样循环了。起初,有LAMBDA
. LAMBDA
开始LET
。LET
begat LET*
,并通过可选参数 init-forms 的顺序绑定LET*
开始更新。LAMBDA
:)
结果是将现代的直接称为 lambda 转换let
为非常复杂。例如:
(funcall (lambda (x y &optional (z x) (w (1+ z))) a b c)
可以编译成:
(let ((x a) (y b)) ;; we put the fixed params into a let
(let* ((z c)) ;; z arg present, so refer to c, not x
(w (1+ z))) ;; w arg absent, use (1+ z)
...))