Common Lisp中的LET与LET *


80

我了解LET和LET *(并行绑定和顺序绑定)之间的区别,从理论上讲,这很有意义。但是,在任何情况下,您实际需要LET吗?在我最近查看的所有Lisp代码中,您可以将所有LET替换为LET *,而无需进行任何更改。

编辑:好的,我知道为什么有人发明了LET *(大概是一个宏),可以追溯到何时。我的问题是,考虑到LET *的存在,LET有理由留在身边吗?您是否编写了任何实际的Lisp代码,而LET *不能像普通LET一样有效?

我不赞成效率论点。首先,认识到可以将LET *编译成与LET一样高效的情况似乎并不难。其次,CL规范中有很多东西似乎根本就不是围绕效率而设计的。(如果是你看到了类型声明循环中的最后一次?这些都是如此很难搞清楚我从来没有见过他们使用)前的1980年代末的迪克·加布里埃尔的基准,CL彻头彻尾的慢。

看来这是向后兼容的另一种情况:明智地,没有人愿意冒险破坏像LET这样的基本功能。这是我的直觉,但是令人欣慰的是,没有人遇到一个愚蠢而简单的案例,而LET使一堆事情比LET *容易得多。


平行是一个不好的选择。仅以前的绑定是可见的。并行绑定将更像Haskell的“ ... where ...”绑定。
jrockway

我的目的不是混淆;我相信这些是规范使用的词。:-)
肯(Ken)

12
平行是正确的。这意味着绑定同时变为现实,彼此看不见且彼此不遮蔽。任何时候都不会存在一个用户可见的环境,其中包括LET中定义的某些变量,而其他变量则不包括。
卡兹(Kaz)2012年

Haskells中的绑定更像letrec。他们可以看到同一作用域级别上的所有绑定。
Edgar Klerks 2014年

1
问“是否有let需要的地方?” 有点像问“是否需要一个带有多个参数的函数?”。 letlet*之所以不存在,是因为有些效率的概念,它们之所以存在,是因为它们允许人们在编程时与其他人交流意图。
tfb

Answers:


85

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))

上面将变量声明ifixnum

理查德·P·加布里埃尔(Richard P. Gabriel)于1985年出版了有关Lisp基准的书,当时这些基准也用于非CL Lisps。Common Lisp本身在1985年是全新的-描述该语言的CLtL1书刚刚在1984年出版。难怪当时的实现还不是很优化。实现的优化与之前的实现(例如MacLisp)基本相同(或更小)。

但是,对于LETvs. LET*,主要区别在于代码使用LET对于人类而言更容易理解,因为绑定子句彼此独立-尤其是因为利用从左到右的评估是不好的风格(不将变量设置为side)影响)。


3
不,不!Lambda不是真正的原语,因为它可以用LET替换,而下层的lambda仅提供一个API来获取参数值: (low-level-lambda 2 (let ((x (car %args%)) (y (cadr args))) ...) :)
Kaz 2012年

36

不需要LET,但通常需要它。

LET建议您只是在进行标准的并行绑定而没有任何棘手的事情。LET *对编译器产生了限制,并向用户建议需要顺序绑定是有原因的。在样式方面,当您不需要LET *施加的额外限制时,LET会更好。

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

  • 并行绑定可以并行执行(但是我不知道是否有任何LISP系统确实这样做,并且init表单仍必须顺序执行)
  • 并行绑定为所有绑定创建一个新的环境(范围)。顺序绑定为每个单个绑定创建一个新的嵌套环境。并行绑定使用较少的内存,并具有更快的变量查找

(以上要点适用于Scheme,其他LISP方言可能有所不同。)


2
注意:请参见此答案(和/或hyperspec的链接部分),以获取有关您的第一个母鸡点为何的误解的信息,可以说。该绑定发生在平行的,但在形式被顺序执行-每个规范。
lindes

6
并行执行不是Common Lisp标准以任何方式来解决的问题。更快的变量查找也是一个神话。
Rainer Joswig'4

1
区别不仅对编译器重要。我用let和let *暗示自己发生了什么。当我在代码中看到let时,我知道绑定是独立的,当我看到let *时,我知道绑定是相互依赖的。但是我只知道这一点,因为我确保始终使用let和let *。
耶利米2014年

28

我来举一些人为的例子。比较结果:

(print (let ((c 1))
         (let ((c 2)
               (a (+ c 1)))
           a)))

运行此结果:

(print (let ((c 1))
         (let* ((c 2)
                (a (+ c 1)))
           a)))

3
关心为什么会出现这种情况?
约翰·麦卡利

4
@John:在第一个示例中,a的绑定引用的外部值c。在第二个示例中,其中的let*绑定允许引用以前的绑定,而a的绑定则引用的内部值c。Logan并没有在意这是人为的例子,甚至也不假装是有用的。而且,缩进是非标准的并且具有误导性。在这两种情况下,a的绑定都应在与s对齐的上方留出一个空格c,而内部的“主体”let应在距其let本身仅留两个空格的位置。
zem

3
这个答案提供了重要的见解。当一个人想要避免第二个绑定(我简单地不是第一个绑定)引用第一个绑定时,您会特别使用它,但是您确实想隐藏一个先前的绑定-使用它的先前值初始化其中一个您的辅助绑定。let
林德斯(Lindes)2012年

2
尽管缩进不可用(我无法编辑),但这是迄今为止最好的示例。当我看(让...)时,我知道没有任何绑定可以相互建立,可以单独处理。当我查看(let * ...)时,我总是谨慎行事,并非常仔细地查看正在重新使用哪些绑定。仅出于这个原因,除非绝对需要嵌套,否则始终使用(let)是有意义的。
安德鲁(Andrew)2012年

1
(6年后...)是设计上的错误和误导性缩进,旨在作为陷阱吗?我倾向于对其进行修改以解决此问题……我不应该吗?
内斯

11

在LISP中,通常希望使用最弱的构造。例如,一些样式指南会告诉您使用=而不是eql当您知道比较的项目是数字时。通常的想法是指定您的意思而不是有效地对计算机进行编程。

但是,仅说出您的意思而不使用更强大的构造可能会真正提高效率。如果您使用进行了初始化LET,那么它们可以并行执行,而LET*初始化则必须顺序执行。我不知道是否有任何实际的实现方式,但是将来可能会实现。


2
好点子。尽管由于Lisp是一种高级语言,但是这使我感到奇怪,为什么“最弱的结构”在Lisp领域是如此受欢迎。您不会看到Perl程序员说“好吧,我们不需要在这里使用正则表达式...” :-)
Ken Ken

1
我不知道,但是有明确的风格偏好。有些人(像我一样)反对,他们喜欢尽可能使用相同的形式(我几乎从不写setq而不是setf)。说出您的意思可能与想法有关。
David Thornley,2009年

=运营商既不强也不弱于eql。这是一个较弱的测试,因为0等于0.0。但是它也更强大,因为拒绝了非数字参数。
哈兹2012年

2
您所指的原理是使用最强的适用原语,而不是最弱的。例如,如果要比较的事物是符号,请使用eq。或者,如果您知道要分配给符号场所,请使用setq。但是,我的许多Lisp程序员也拒绝了这一原则,他们只是想要一种高级语言而不进行过早的优化。
哈兹2012年

1
实际上,CLHS说绑定是并行完成的”,但“表达式init-form-1init-form-2等等在[特定的,从左至右(或顶部) -down)]订单”。因此,必须按顺序计算值(所有值都计算完后才建立绑定)。同样有意义,因为类似RPLACD的结构突变是该语言的一部分,并且具有真正的并行性,它将变得不确定。
内斯

9

我最近正在编写一个包含两个参数的函数,如果我们知道哪个参数更大,则算法会最清晰地表达。

(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以相同的值。


1
除非稍后需要在外部内部使用x和y的值,否则let可以使用以下方法更简单(更清楚)地完成此操作:(rotatef x y)-这不是一个坏主意,但看起来仍然很麻烦。
肯(Ken)

确实如此。如果x和y是特殊变量,则可能会更有用。
塞缪尔·埃德温·沃德

9

LET和LET *之间的公共列表的主要区别在于,LET中的符号是并行绑定的,而LET *中的符号则是顺序绑定的。使用LET不允许并行执行init-form,也不允许更改init-form的顺序。原因是Common Lisp允许函数产生副作用。因此,评估顺序很重要,并且始终在表单内从左到右。因此,在LET中,首先从左到右评估init形式,然后从左到右并行创建绑定。在LET *中,将评估init形式,然后按从左到右的顺序绑定到符号。

CLHS:特殊运算符LET,LET *


2
似乎这个答案可能正在从对这个答案的回应中汲取一些能量?另外,按照链接的规范,即使您正确地指出init-forms是按顺序执行的,也可以说绑定是在中并行完成的LET。我不知道这在任何现有的实现中是否有任何实际区别。
lindes

8

我走一步,使用绑定统一了letlet*multiple-value-binddestructuring-bind等,它甚至扩展。

通常,我喜欢使用“最弱的结构”,但是不喜欢与let&朋友一起使用,因为它们只会给代码带来噪音(主观性警告!无需尝试说服我……)


哦,整洁。我现在要去玩BIND。感谢您的链接!

4

据推测,通过使用let编译器可以更灵活地对代码进行重新排序,也许是为了提高空间或速度。

从风格上讲,使用并行绑定表明了将绑定分组在一起的意图。有时用于保留动态绑定:

(let ((*PRINT-LEVEL* *PRINT-LEVEL*)
      (*PRINT-LENGTH* *PRINT-LENGTH*))
  (call-functions that muck with the above dynamic variables)) 

在90%的代码中,无论您使用LET还是都没有关系LET*。因此,如果使用*,则会添加不必要的字形。如果LET*是并行绑定程序,而又LET是串行绑定程序,则程序员仍将使用LET,并且仅LET*在需要并行绑定时退出。这可能会变得LET*罕见。
哈兹2012年

实际上,CLHS指定了let的init-form的评估顺序
Will Ness '18

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

当然,这可以工作:

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

还有这个:

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

但重要的是思想。


2

OP询问“实际需要的LET”吗?

创建Common Lisp时,各种各样的方言中都有大量的现有Lisp代码。设计Common Lisp的人们所接受的简介是创建Lisp的方言,以提供共同的基础。他们“需要”使将现有代码移植到Common Lisp中变得容易且有吸引力。将LET或LET *排除在语言之外可能还具有其他优点,但它会忽略该关键目标。

我优先使用LET *而不是LET *,因为它告诉读者一些有关数据流如何展开的信息。至少在我的代码中,如果看到LET *,则知道早期绑定的值将在以后的绑定中使用。我“需要”这样做吗?但我认为这很有帮助。就是说,我很少阅读默认为LET *的代码,而LET的出现表明作者确实想要它。即例如交换两个变量的含义。

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

有一种可争议的方案接近“实际需求”。它与宏一起出现。这个宏:

(defmacro M1 (a b c)
 `(let ((a ,a)
        (b ,b)
        (c ,c))
    (f a b c)))

(defmacro M2 (a b c)
  `(let* ((a ,a)
          (b ,b)
          (c ,c))
    (f a b c)))

因为(M2 cba)无法解决。但是由于各种原因,这些宏非常草率;因此破坏了“实际需要”的论点。


1

除了Rainer Joswig的回答,还是从纯粹主义或理论的角度来看。Let&Let *代表两个编程范例;功能和顺序。

至于为什么我应该继续使用Let *而不是Let,那么,您是从我的家中带回乐趣并以纯功能语言进行思考,而不是我大部分时间都在使用顺序语言的顺序语言:)


1

通过让您使用并行绑定,

(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)

是的,这就是它们的定义方式。但是什么时候需要前者呢?我认为,您实际上并不是在编写需要按​​特定顺序更改pi值的程序。:-)
肯(Ken)

1

let运营商介绍了所有它指定绑定一个单一的环境。let*,至少在概念上(并且如果我们暂时忽略声明)会引入多个环境:

也就是说:

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

就好像:

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

因此从某种意义上讲let是更原始的,而let*在写let-s级联时则是语法糖。

但是没关系。(下面我将为为什么我们“没关系”给出理由)。事实是有两个运算符,在代码的“ 99%”中,使用哪一个都没有关系。之所以喜欢letlet*是因为它最后没有*悬挂。

只要您有两个运算符,而一个运算符*的名称悬空,*则在这种情况下使用不带的运算符,以使您的代码不那么难看。

这就是全部。

话虽如此,我怀疑如果letlet*交换它们的含义,它可能let*比现在更罕见。串行绑定行为不会打扰大多数不需要它的代码:很少会let*被用来请求并行行为(而且这种情况也可以通过重命名变量来避免阴影来解决)。

现在,进行有希望的讨论。尽管let*从概念上引入了多个环境,但是编译let*构造以生成单个环境非常容易。因此,即使(至少如果我们忽略ANSI CL声明),也存在一个代数相等,即一个let*对应于多个嵌套let-s,这使它let看起来更加原始,也没有理由真正扩展let*到对其let进行编译,甚至没有理由馊主意。

还有一件事:请注意,Common Lisplambda实际上使用的是let*类似语义!例:

(lambda (x &optional (y x) (z (+1 y)) ...)

在这里,x用于y访问早期x参数的init-form形式类似地(+1 y)引用了早期的y可选参数。在语言的这一方面,明确显示了对绑定中的顺序可见性的偏好。如果中的表格lambda看不到参数,则将不太有用;就先前参数的值而言,可选参数不能被简洁地默认。


0

在下面let,所有变量初始化表达式都具有完全相同的词法环境:包围着let。如果这些表达式碰巧捕获了词法闭包,则它们都可以共享同一环境对象。

在下let*,每个初始化表达式都在不同的环境中。对于每个连续的表达式,必须扩展环境以创建一个新的表达式。至少在抽象语义上,如果捕获了闭包,则它们具有不同的环境对象。

let*必须对A进行充分优化,以消除不必要的环境扩展,以适合作为的日常替代let。必须有一个编译器,该编译器的工作方式是访问哪些表单,然后将所有独立的表单转换为更大的,组合的let

(即使let*只是发出级联let形式的宏运算符,也是如此;优化是在级联的lets上完成的)。

您不能使用隐藏变量赋值来实现let*单个naivelet来进行初始化,因为这样会显示出缺乏适当的作用域:

(let* ((a (+ 2 b))  ;; b is visible in surrounding env
       (b (+ 3 a)))
  forms)

如果变成

(let (a b)
  (setf a (+ 2 b)
        b (+ 3 a))
  forms)

在这种情况下将不起作用;内部b遮蔽了外部,b所以我们最后将2加到上nil。如果我们对所有这些变量进行alpha重命名,可以完成这种转换。然后将环境很好地展平:

(let (#:g01 #:g02)
  (setf #:g01 (+ 2 b) ;; outer b, no problem
        #:g02 (+ 3 #:g01))
  alpha-renamed-forms) ;; a and b replaced by #:g01 and #:g02

为此,我们需要考虑调试支持。如果程序员使用调试器进入此词法范围,我们是否希望他们处理#:g01而不是a

因此,从根本上说,let*是复杂的结构,必须对其进行优化以使其性能良好,并且let在可能降低到的情况下也是如此let

这本身就不能证明有利于letlet*。假设我们有一个好的编译器;为什么不一直使用let*

作为一般原则,与易于出错的低级结构相比,我们应该偏向于使我们具有生产力并减少错误的高级别结构,并尽可能依赖于高级别结构的良好实现,因此我们几乎不必牺牲为了性能而使用它们。这就是为什么我们首先使用Lisp这样的语言进行工作的原因。

该推理不适用于letvs let*,因为相对于,let*显然不是更高层次的抽象let。他们是关于“平等水平”的。使用let*,您可以引入只需切换到即可解决的错误let。而反之亦然let*实际上,它只是用于视觉折叠let嵌套的温和语法糖,而不是重要的新抽象。


-1

我主要使用LET,除非我特别需要LET *,但是有时我编写明确需要LET的代码,通常是在进行分类(通常是复杂的)默认设置时使用。不幸的是,我手边没有任何方便的代码示例。


-1

谁又想重新编写letf vs letf *?展开保护电话的数量?

更容易优化顺序绑定。

也许它会影响ENV

允许动态范围的延续?

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

[我认为有效]的重点是,有时越简单越容易阅读。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.