我理解LET和LET*之间的区别(并行与顺序绑定),作为理论问题,它非常有意义.但有没有你真的需要LET的情况?在我最近看过的所有Lisp代码中,您可以用LET*替换每个LET而不做任何更改.
编辑:好的,我理解为什么有些人发明了LET*,大概是作为一个宏,回归的时候.我的问题是,鉴于LET*存在,是否有理由让LET留在身边?您是否编写了任何实际的Lisp代码,其中LET*不能像普通的LET那样工作?
我不买效率论证.首先,认识到LET*可以编译成像LET一样高效的情况,这似乎并不难.其次,CL规范中有很多东西似乎根本就不是围绕效率而设计的.(当你最后一次看到带有类型声明的循环时?那些很难弄清楚我从未见过它们使用过.)在20世纪80年代后期的Dick Gabriel基准测试之前,CL 非常缓慢.
看起来这是另一种向后兼容的情况:明智的是,没有人愿意冒险破坏像LET那样基本的东西.这是我的预感,但是听到没有人有一个我丢失的愚蠢简单的案例令人欣慰,因为LET比LET*更容易让事情变得简单.
Rai*_*wig 80
LET它本身并不是函数式编程语言中的真正原语,因为它可以替代LAMBDA.像这样:
(let ((a1 b1) (a2 b2) ... (an bn))
(some-code a1 a2 ... an))
Run Code Online (Sandbox Code Playgroud)
类似于
((lambda (a1 a2 ... an)
(some-code a1 a2 ... an))
b1 b2 ... bn)
Run Code Online (Sandbox Code Playgroud)
但
(let* ((a1 b1) (a2 b2) ... (an bn))
(some-code a1 a2 ... an))
Run Code Online (Sandbox Code Playgroud)
类似于
((lambda (a1)
((lambda (a2)
...
((lambda (an)
(some-code a1 a2 ... an))
bn))
b2))
b1)
Run Code Online (Sandbox Code Playgroud)
你可以想象哪个更简单.LET而不是LET*.
LET使代码理解更容易.人们可以看到一堆绑定,并且可以单独读取每个绑定,而无需了解"效果"(重新绑定)的自上而下/左右流动.使用LET*信号给程序员(读取代码的程序员),绑定不是独立的,但存在某种自上而下的流程 - 这使事情变得复杂.
Common Lisp具有LET从左到右计算绑定值的规则.只是如何评估函数调用的值 - 从左到右.因此,LET概念上是更简单的语句,默认情况下应该使用它.
类型LOOP?经常使用.有一些原始形式的类型声明很容易记住.例:
(LOOP FOR i FIXNUM BELOW (TRUNCATE n 2) do (something i))
Run Code Online (Sandbox Code Playgroud)
上面声明变量i为a fixnum.
Richard P. Gabriel在1985年发表了关于Lisp基准测试的书,当时这些基准测试也用于非CL Lisps.Common Lisp本身在1985年是全新的 - CLtL1书中描述了该语言刚刚于1984年出版.难怪当时的实现并没有得到很好的优化.实现的优化与之前的实现基本相同(或更少)(如MacLisp).
但LET与LET*主要的区别是使用的代码LET更容易理解对于人类来说,由于约束力的条款是相互独立的-尤其是因为它是坏的风格趁从左到右评估(未设置变量的一个侧面影响).
Mr *_*ooz 33
你不需要 LET,但你通常需要它.
LET表明你只是做标准的并行绑定而没有任何棘手的事情.LET*引起对编译器的限制,并向用户建议有必要进行顺序绑定的原因.在风格方面,当您不需要LET*施加的额外限制时,LET会更好.
使用LET比使用LET*更高效(取决于编译器,优化器等):
(上述要点适用于Scheme,另一种LISP方言.clisp可能有所不同.)
Log*_*ldo 24
我带来了人为的例子.比较结果:
(print (let ((c 1))
(let ((c 2)
(a (+ c 1)))
a)))
Run Code Online (Sandbox Code Playgroud)
运行结果:
(print (let ((c 1))
(let* ((c 2)
(a (+ c 1)))
a)))
Run Code Online (Sandbox Code Playgroud)
Dav*_*ley 11
在LISP中,通常需要使用最弱的构造.例如,某些样式指南会告诉您使用=而不是eql当您知道比较的项目是数字时.这个想法通常是指明你的意思,而不是有效地编程计算机.
但是,只能说出你的意思,而不是使用更强大的结构,可以实际提高效率.如果使用了初始化LET,则可以并行执行,而LET*初始化必须按顺序执行.我不知道是否有任何实现实际上会这样做,但有些可能在未来.
小智 9
LET和LET*之间的公共列表的主要区别在于LET中的符号是并行绑定的,而LET*中的符号是顺序绑定的.使用LET不允许init-forms并行执行,也不允许更改init-forms的顺序.原因是Common Lisp允许函数产生副作用.因此,评估的顺序很重要,并且在表格中始终是从左到右.因此,在LET中,首先从左到右评估init-forms,然后并行地从左到右创建绑定.在LET*中,初始形式被评估,然后从左到右依次绑定到符号.
我最近编写了两个参数的函数,如果我们知道哪个参数更大,那么算法表达得最清楚.
(defun foo (a b)
(let ((a (max a b))
(b (min a b)))
; here we know b is not larger
...)
; we can use the original identities of a and b here
; (perhaps to determine the order of the results)
...)
Run Code Online (Sandbox Code Playgroud)
假设b更大,如果我们使用let*,我们会不小心设置a和b相同的值.
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 编译器都可以轻松生成环境的有效表示,而解释器则不一定如此。
既然解释器是第一位的,这就解释了为什么let是并行的。至少部分如此。另一个原因是它let被实现为语法糖lambda。但是lambda(最初)在它自己的范围内根本没有初始化表达式;它只是指定变量。该lambda表达式生成一个运行时对象,以便在运行时调用函数时将值绑定到参数。参数表达式的求值处于完全不同的范围内。
现在,在立即调用的 中lambda,这仍然是正确的:表达式的范围完全超出了lambda:
((lambda (a b) (+ a b)) 1 2)
Run Code Online (Sandbox Code Playgroud)
表达式1和2与 无关lambda;他们没有被包围在其中。
所以很明显,如果我们想要一个let与上面相对应的糖表示法,我们必须小心地保留这个属性:
(let ((a 1) (b 2))
(+ a b))
Run Code Online (Sandbox Code Playgroud)
如果我们希望它let与前面的相同lambda,我们必须使它看起来好像a和b是函数参数,并且1和2是参数表达式。
如果您是一名研究人员,正在使用一种有lambda或没有 的语言let,并且渴望一种更好的方式来编写立即调用的lambdas,那么您不太可能发明let*绑定语义。您将发明一些对您在整个代码中使用的现有构造具有清晰的翻译策略的东西,以便您可以重构代码以毫无意外地使用它。
请注意,像 Common Lisp 这样的现代lambda方言中确实嵌入了表达式:即可选参数和关键字参数!
(lambda (a &optional (b x) (c y) ...))
Run Code Online (Sandbox Code Playgroud)
当参数丢失时,每次调用函数时,这些默认值表达式x和都会在周围的词法范围内求值。y那么,这些表达式使用什么范围规则呢?为什么是串行,而不是并行!
[1]> (defun foo (x &optional (y (1+ x)) (z (1+ y)))
(list x y z))
FOO
[2]> (foo 10)
(10 11 12)
Run Code Online (Sandbox Code Playgroud)
于是,事情又回到了原点。一开始,有LAMBDA。LAMBDA产生了LET。LETbegatLET*和LET*begat newerLAMBDA具有可选参数 init-forms 的顺序绑定。:)
结果是将现代的立即调用的 lambda 翻译成let相当复杂。例如:
(funcall (lambda (x y &optional (z x) (w (1+ z))) a b c)
Run Code Online (Sandbox Code Playgroud)
可以编译成:
(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)
...))
Run Code Online (Sandbox Code Playgroud)
(let ((list (cdr list))
(pivot (car list)))
;quicksort
)
Run Code Online (Sandbox Code Playgroud)
当然,这会起作用:
(let* ((rest (cdr list))
(pivot (car list)))
;quicksort
)
Run Code Online (Sandbox Code Playgroud)
和这个:
(let* ((pivot (car list))
(list (cdr list)))
;quicksort
)
Run Code Online (Sandbox Code Playgroud)
但最重要的是想法。