我理解LET和LET*之间的区别(并行与顺序绑定),作为理论问题,它非常有意义.但有没有你真的需要LET的情况?在我最近看过的所有Lisp代码中,您可以用LET*替换每个LET而不做任何更改.
编辑:好的,我理解为什么有些人发明了LET*,大概是作为一个宏,回归的时候.我的问题是,鉴于LET*存在,是否有理由让LET留在身边?您是否编写了任何实际的Lisp代码,其中LET*不能像普通的LET那样工作?
我不买效率论证.首先,认识到LET*可以编译成像LET一样高效的情况,这似乎并不难.其次,CL规范中有很多东西似乎根本就不是围绕效率而设计的.(当你最后一次看到带有类型声明的循环时?那些很难弄清楚我从未见过它们使用过.)在20世纪80年代后期的Dick Gabriel基准测试之前,CL 非常缓慢.
看起来这是另一种向后兼容的情况:明智的是,没有人愿意冒险破坏像LET那样基本的东西.这是我的预感,但是听到没有人有一个我丢失的愚蠢简单的案例令人欣慰,因为LET比LET*更容易让事情变得简单.
LET
它本身并不是函数式编程语言中的真正原语,因为它可以替代LAMBDA
.像这样:
(let ((a1 b1) (a2 b2) ... (an bn)) (some-code a1 a2 ... an))
类似于
((lambda (a1 a2 ... an) (some-code a1 a2 ... an)) b1 b2 ... bn)
但
(let* ((a1 b1) (a2 b2) ... (an bn)) (some-code a1 a2 ... an))
类似于
((lambda (a1) ((lambda (a2) ... ((lambda (an) (some-code a1 a2 ... an)) bn)) b2)) b1)
你可以想象哪个更简单.LET
而不是LET*
.
LET
使代码理解更容易.人们可以看到一堆绑定,并且可以单独读取每个绑定,而无需了解"效果"(重新绑定)的自上而下/左右流动.使用LET*
信号给程序员(读取代码的程序员),绑定不是独立的,但存在某种自上而下的流程 - 这使事情变得复杂.
Common Lisp具有LET
从左到右计算绑定值的规则.只是如何评估函数调用的值 - 从左到右.因此,LET
概念上是更简单的语句,默认情况下应该使用它.
类型LOOP
?经常使用.有一些原始形式的类型声明很容易记住.例:
(LOOP FOR i FIXNUM BELOW (TRUNCATE n 2) do (something i))
上面声明变量i
为a fixnum
.
Richard P. Gabriel在1985年发表了关于Lisp基准测试的书,当时这些基准测试也用于非CL Lisps.Common Lisp本身在1985年是全新的 - CLtL1书中描述了该语言刚刚于1984年出版.难怪当时的实现并没有得到很好的优化.实现的优化与之前的实现基本相同(或更少)(如MacLisp).
但LET
与LET*
主要的区别是使用的代码LET
更容易理解对于人类来说,由于约束力的条款是相互独立的-尤其是因为它是坏的风格趁从左到右评估(未设置变量的一个侧面影响).
你不需要 LET,但你通常需要它.
LET表明你只是做标准的并行绑定而没有任何棘手的事情.LET*引起对编译器的限制,并向用户建议有必要进行顺序绑定的原因.在风格方面,当您不需要LET*施加的额外限制时,LET会更好.
使用LET比使用LET*更高效(取决于编译器,优化器等):
并行绑定可以并行执行(但我不知道是否有任何LISP系统实际执行此操作,并且init表单仍必须按顺序执行)
并行绑定为所有绑定创建单个新环境(范围).顺序绑定为每个单独的绑定创建一个新的嵌套环境.并行绑定使用更少的内存并具有更快的变量查找.
(上述要点适用于Scheme,另一种LISP方言.clisp可能有所不同.)
我带来了人为的例子.比较结果:
(print (let ((c 1)) (let ((c 2) (a (+ c 1))) a)))
运行结果:
(print (let ((c 1)) (let* ((c 2) (a (+ c 1))) a)))
在LISP中,通常需要使用最弱的构造.例如,某些样式指南会告诉您使用=
而不是eql
当您知道比较的项目是数字时.这个想法通常是指明你的意思,而不是有效地编程计算机.
但是,只能说出你的意思,而不是使用更强大的结构,可以实际提高效率.如果使用了初始化LET
,则可以并行执行,而LET*
初始化必须按顺序执行.我不知道是否有任何实现实际上会这样做,但有些可能在未来.
LET和LET*之间的公共列表的主要区别在于LET中的符号是并行绑定的,而LET*中的符号是顺序绑定的.使用LET不允许init-forms并行执行,也不允许更改init-forms的顺序.原因是Common Lisp允许函数产生副作用.因此,评估的顺序很重要,并且在表格中始终是从左到右.因此,在LET中,首先从左到右评估init-forms,然后并行地从左到右创建绑定.在LET*中,初始形式被评估,然后从左到右依次绑定到符号.
CLHS:特别运营商LET,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) ...)
假设b
更大,如果我们使用let*
,我们会不小心设置a
和b
相同的值.
我走一步,并使用绑定统一了let
,let*
,multiple-value-bind
,destructuring-bind
等,它甚至扩展.
一般来说,我喜欢使用"最弱的构造",但不喜欢let
和朋友一起使用,因为它们只是给代码带来了噪音(主观性警告!不需要试着说服我相反的......)