LET与LET *在Common Lisp中

我理解LET和LET *之间的区别(并行与顺序绑定),作为一个理论问题,它是非常有意义的。 但是有没有什么情况下你真的需要LET? 在我最近研究过的所有的Lisp代码中,都可以用LET *replace每个LET,而不会改变。

编辑:好的,我明白为什么有些人发明了LET *,大概是一个macros,回头的时候。 我的问题是,鉴于LET *的存在,是否有理由让LET呆在附近? 你有没有写出任何实际的Lisp代码,LET *不能和普通的LET一样工作?

我不买效率的说法。 首先,认识到LET *可以被编译成像LET一样有效的情况似乎并不困难。 其次,在CL规范中有很多东西,根本不像是围绕效率devise的。 (当你最后一次看到types声明的LOOP时,那些是很难弄清楚的,我从来没有见过它们被使用过。)在迪克·加布里埃尔(Dick Gabriel)20世纪80年代后期的基准之前,CL 非常缓慢的。

看起来这是另一个向后兼容的情况:明智的是,没有人愿意冒险打破像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*

让我们更容易理解代码。 我们可以看到一堆绑定,并且可以单独读取每个绑定,而不需要了解“效果”(重新绑定)的自上而下/左右stream。 使用LET*信号给程序员(读代码的人)绑定不是独立的,但有某种自上而下的stream程 – 这使事情变得复杂。

Common Lisp的规则是, LET中绑定的值从左到右计算。 只是如何评估函数调用的值 – 从左到右。 所以, LET在概念上是比较简单的,它应该默认使用。

LOOPtypes ? 经常使用。 有一些原始的types声明forms很容易记住。 例:

 (LOOP FOR i FIXNUM BELOW (TRUNCATE n 2) do (something i)) 

上面声明variablesi是一个fixnum

Richard P. Gabriel于1985年发表了他的关于Lisp基准的书,当时这些基准也用于非CL Lisp。 Common Lisp本身在1985年是全新的 ,描述语言的CLtL1书籍刚刚在1984年出版。毫无疑问,当时的实现并没有得到很好的优化。 实现的优化与之前的实现(如MacLisp)基本相同(或更less)。

但是对于LETLET* ,主要区别在于使用LET代码对于人类来说更容易理解,因为绑定条款彼此独立 – 尤其是因为利用左向右评估(不设置variables作为副作用)。

不需要让,但你通常需要它。

让我们build议你只是做标准的并行绑定,没有任何棘手的事情发生。 LET *引起对编译器的限制,并向用户build议有一个原因需要顺序绑定。 就风格而言 ,当你不需要LET *所施加的额外限制时,LET更好。

使用LET比LET *更有效率(取决于编译器,优化器等):

  • 并行绑定可以并行执行 (但我不知道是否有任何LISP系统实际执行此操作,并且init表单仍必须按顺序执行)
  • 并行绑定为所有绑定创build一个新的环境(范围)。 顺序绑定为每个绑定创build一个新的嵌套环境。 并行绑定使用更less的内存,并具有更快的variables查找

(上面的要点适用于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不允许并行执行initforms,也不允许改变initforms的顺序。 原因是Common Lisp允许函数有副作用。 因此,评价的顺序是重要的,并且始终是从一个表格中左向右的。 因此,在LET中,init表单首先被评估,从左到右,然后绑定被创build,从左到右。 在LET *中,初始化表单被评估,然后从左到右依次绑定到符号。

CLHS:特种运营商LET,LET *

我最近写了两个参数的函数,如果我们知道哪个参数较大,algorithm就expression得最清楚。

 (defun foo (ab) (let ((a (max ab)) (b (min ab))) ; 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* ,我们会不小心将ab设置为相同的值。

我更进一步,使用绑定 ,统一让,让*,多值绑定,解构绑定等,它甚至是可扩展的。

一般我喜欢用“最薄弱的结构”,但是不要和let和friends打交道,因为他们只是给代码添加噪音(主观性警告!不需要试图说服我相反…)

据推测,通过let编译器具有更大的灵活性来重新sorting代码,可能是为了空间或速度的提高。

在风格上,使用并行绑定显示绑定被组合在一起的意图; 这有时用于保留dynamic绑定:

 (let ((*PRINT-LEVEL* *PRINT-LEVEL*) (*PRINT-LENGTH* *PRINT-LENGTH*)) (call-functions that muck with the above dynamic variables)) 
 (let ((list (cdr list)) (pivot (car list))) ;quicksort ) 

当然,这将工作:

 (let* ((rest (cdr list)) (pivot (car list))) ;quicksort ) 

和这个:

 (let* ((pivot (car list)) (list (cdr list))) ;quicksort ) 

但这是重要的思想。

OP询问“实际上是否需要LET”?

当Common Lisp被创build时,用各种各样的方言来载入现有的Lisp代码。 Lisp接受devise的人的简介是创build一个Lisp方言,提供共同的基础。 他们“需要”将现有代码移植到Common Lisp中,使其变得简单而有吸引力。 离开LET或LET *语言本身可能有其他的美德,但它会忽略这个关键的目标。

我使用LET优先于LET *,因为它告诉读者有关数据stream如何展开的内容。 在我的代码中,至less,如果你看到一个LET *,你知道早期绑定的值将在以后的绑定中使用。 我是否需要这样做? 但我认为这是有帮助的。 也就是说,我很less看到默认为LET *的代码以及作者真正想要的LET信号的外观。 例如,交换两个variables的含义。

 (let ((good bad) (bad good) ...) 

有争议的情况接近“实际需要”。 它出现在macros。 这个macros:

 (defmacro M1 (abc) `(let ((a ,a) (b ,b) (c ,c)) (fabc))) 

比…更好

 (defmacro M2 (abc) `(let* ((a ,a) (b ,b) (c ,c)) (fabc))) 

因为(M2 cba)是不会解决的。 但是这些macros由于各种各样的原因相当sl </s>, 这就破坏了“实际需要”的论点。

除了Rainer Joswig的回答,还有从纯粹主义或理论的angular度来看。 Let&Let *表示两种编程模式; function和顺序分别。

至于为什么我应该继续使用Let *而不是Let,那么,你正在从我回家的乐趣中思考纯粹的函数式语言,而不是顺序语言,我花了大部分时间在一起工作:)

让你使用并行绑定,

 (setq my-pi 3.1415) (let ((my-pi 3) (old-pi my-pi)) (list my-pi old-pi)) => (3 3.1415) 

用Let *串口绑定,

 (setq my-pi 3.1415) (let* ((my-pi 3) (old-pi my-pi)) (list my-pi old-pi)) => (3 3) 

let运算符为它指定的所有绑定引入单个环境。 let* ,至less在概念上(如果我们暂时忽略声明)引入多个环境:

也就是说:

 (let* (abc) ...) 

就好像:

 (let (a) (let (b) (let (c) ...))) 

所以从某种意义上说, let是更原始的,而let*是写一个let -s级联的语法糖。

但是没关系。 事实上有两个运营商,在“99%”的代码中,你使用哪一个并不重要。 宁愿let let*的原因就是它最后没有*悬挂。

每当你有两个操作符,一个名字是*如果在这种情况下工作,就使用没有*操作符,以使代码不那么难看。

这就是全部。

话虽如此,我怀疑如果let let*交换意思,那么使用let*比现在更为罕见。 串行绑定行为不会影响大多数不需要它的代码:很less会用let*来请求并行行为(这种情况也可以通过重命名variables来避免影响)。

我主要使用LET,除非我特别需要LET *,但有时候我会编写明确需要 LET的代码,通常在做各种(通常是复杂的)违约时。 不幸的是,我手边没有任何方便的代码示例。

谁感觉像重新编写letf再letf *? 放松保护电话的数量?

更容易优化顺序绑定。

也许它影响环境?

允许dynamic范围的延续?

有时(let(xyz)(setq z 0 y 1 x(+(setq x 1)(prog1(+ xy)(setq x(1-x))))(values()))

[我认为作品]的一点是,简单易读有时候。