let 和let* 之间肯定存在效率争论。但我们之所以拥有let 的主要原因是历史性的,因为与lambda 的关系。
let 更容易、更简单、更高效地实现在代码遍历 Lisp 解释器中。如果环境有一些半体面的数据结构,而不仅仅是assoc 列表,则尤其如此。
假设解释器将环境实现为对象链。所以对于不是这样(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* 的解释器性能之间的差异,但只能将let 的性能拖到let*。如果我们将环境链表示为一个幼稚的assoc 列表,那么这个问题无关紧要;所有变量查找都是线性搜索。事实上,let* 更容易实现:评估每个 init 表达式,并将新绑定推送到当前环境。
现在,在图片中加入编译。 Lisp 编译器可以使用魔鬼技巧来实现let*,只需对let 的编译策略进行一些调整。为了编译let*,我们可以为所有绑定分配一个单一的环境(这会导致解释器中的范围不正确)。我们将该环境留空,并将其添加到编译时环境链中。因此,我们在该新环境的范围内编译 init 表达式。当我们遍历 init 表达式来编译它们时,我们将每个对应的变量一一添加到该环境中,以便后续 init 表达式的编译将在范围内具有该变量。
let* 是一个简单的 hack,当你有一个处理 let 的 Lisp 编译器时,它变得很明显。
Lisp 编译器很容易生成环境的有效表示,而不管范围规则如何,解释器不一定如此。
既然解释器是第一位的,这就解释了为什么let 是并行的。至少部分。另一个原因是let 被实现为lambda 的语法糖。但是lambda(最初)在它自己的范围内根本没有初始化表达式;它只是指定变量。 lambda 表达式生成一个运行时对象,以便在调用函数时在运行时将值绑定到参数。参数表达式的评估在完全不同的范围内。
现在在立即调用的lambda 中,这仍然是正确的:表达式的范围完全在lambda 之外:
((lambda (a b) (+ a b)) 1 2)
表达式1 和2 与lambda 无关;它们没有包含在其中。
所以很明显,如果我们想要一个与上述相对应的let糖表示法,我们必须小心地保留这个属性:
(let ((a 1) (b 2))
(+ a b))
如果我们想让这个let和前面的lambda一样,我们必须让它看起来a和b是函数参数,而1和2是参数表达式.
如果您是一名研究人员,使用的语言有lambda 而没有let,渴望有一种更好的方式来立即编写,称为lambdas,那么您不太可能发明let* 绑定语义。您将发明一些对您在整个代码中使用的现有结构具有清晰翻译策略的东西,以便您可以重构代码以毫无意外地使用它。
请注意,Common Lisp 等方言中的现代 lambda确实 中嵌入了表达式:即可选参数和关键字参数!
(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 开始 LET* 和 LET* 开始更新 LAMBDA 与可选参数 init-forms 的顺序绑定。 :)
结果是将现代直接调用的 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)
...))