Common Lisp中的LET与LET*

Ken*_*Ken 78 lisp common-lisp

我理解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).

LETLET*主要的区别是使用的代码LET更容易理解对于人类来说,由于约束力的条款是相互独立的-尤其是因为它是坏的风格趁从左到右评估(未设置变量的一个侧面影响).

  • 不,不!Lambda不是真正的原语,因为它可以用LET替换,而低级lambda只提供API来获取参数值:`(低级-lambda 2(let((x(car%args%) ))(y(cadr args)))...)`:) (3认同)

Mr *_*ooz 33

不需要 LET,但你通常需要它.

LET表明你只是做标准的并行绑定而没有任何棘手的事情.LET*引起对编译器的限制,并向用户建议有必要进行顺序绑定的原因.在风格方面,当您不需要LET*施加的额外限制时,LET会更好.

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

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

(上述要点适用于Scheme,另一种LISP方言.clisp可能有所不同.)

  • 并行执行不是Common Lisp标准以任何方式处理的事情.更快的变量查找也是一个神话. (5认同)
  • 注意:请参阅[此答案](http://stackoverflow.com/a/562975/313756)(和/或hyperspec的链接部分),了解您的第一个后备点的原因 - 误导,让我们说._bindings_并行发生,但_forms_按顺序执行 - 按规范执行. (2认同)
  • 这种差异不仅对编译器很重要。我使用 let 和 let* 来提示自己正在发生的事情。当我在代码中看到 let 时,我知道绑定是独立的,而当我看到 let* 时,我知道绑定是相互依赖的。但我只知道这一点,因为我确保一致地使用 let 和 let* 。 (2认同)

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)

  • @John:在第一个例子中,`a`的绑定指的是`c`的外部值.在第二个例子中,`let*`允许绑定引用先前的绑定,`a`的绑定指的是`c`的内部值.洛根并不是在说这是一个人为的例子,它甚至没有假装有用.此外,缩进是非标准和误导性的.在两者中,`a`的绑定应该是一个空格,与`c`排列,内部`let`的'body'应该只是'let`本身的两个空格. (3认同)
  • 这个答案提供了重要的见解.当一个人想要_avoid_具有二级绑定(我只是意味着不是第一个)时,会特别**使用`let`引用第一个绑定,但是你想要隐藏一个先前的绑定 - 使用前一个初始化一个辅助绑定的值. (3认同)
  • 关心开发为什么会这样? (2认同)
  • 尽管缩进已关闭(而且我无法对其进行编辑),但这是迄今为止最好的示例。当我查看 (let ...) 时,我知道 *none* 绑定将相互构建并且可以单独处理。当我查看 (let* ...) 时,我总是小心翼翼地接近并非常仔细地查看哪些绑定正在被重用。仅出于这个原因,除非绝对需要嵌套,否则始终使用 (let) 是有意义的。 (2认同)
  • (6 年后...)是设计上错误且具有误导性的缩进,旨在作为*陷阱*?我倾向于编辑它来修复它......我不应该吗? (2认同)

Dav*_*ley 11

在LISP中,通常需要使用最弱的构造.例如,某些样式指南会告诉您使用=而不是eql当您知道比较的项目是数字时.这个想法通常是指明你的意思,而不是有效地编程计算机.

但是,只能说出你的意思,而不是使用更强大的结构,可以实际提高效率.如果使用了初始化LET,则可以并行执行,而LET*初始化必须按顺序执行.我不知道是否有任何实现实际上会这样做,但有些可能在未来.

  • 好点子.虽然Lisp是一种高级语言,但这让我想知道为什么"最弱的构造"在Lisp土地上是如此理想的风格.你没有看到Perl程序员说"好吧,我们不需要*在这里使用regexp ......":-) (2认同)
  • 你所指的原则是使用*最强*适用的原语,而不是*最弱*.例如,如果要比较的东西是符号,请使用`eq`.或者,如果您知道要分配给符号位置,请使用`setq`.但是,这个原则也被我的许多Lisp程序员拒绝了,他们只想要一个没有过早优化的高级语言. (2认同)
  • 实际上,[CLHS 说](http://www.lispworks.com/documentation/lw60/CLHS/Body/s_let_l.htm)“*绑定*[是并行完成的]”但是“表达式*init-form- 1*、*init-form-2* 等,[按[特定的、从左到右(或从上到下)] 的顺序进行评估”。因此,必须按顺序计算这些值(*在*计算完所有值之后才建立绑定)。这也是有道理的,因为类似 RPLACD 的结构突变是语言的一部分,并且有了真正的并行性,它就会变得不确定。 (2认同)

小智 9

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

CLHS:特别运营商LET,LET*

  • 似乎这个答案可能是对[这个答案](http://stackoverflow.com/a/555136/313756)的回应所吸取的一些能量?另外,根据规范链接,_bindings_被称为在"LET"中并行完成,即使你正确地指出init-forms是串行执行的.在现有的任何实施中,这是否有任何实际差异,我不知道. (2认同)

Sam*_*ard 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)
  ...)
Run Code Online (Sandbox Code Playgroud)

假设b更大,如果我们使用let*,我们会不小心设置ab相同的值.


Att*_*vai 8

我走一步,并使用绑定统一了let,let*,multiple-value-bind,destructuring-bind等,它甚至扩展.

一般来说,我喜欢使用"最弱的构造",但不喜欢let和朋友一起使用,因为它们只是给代码带来了噪音(主观性警告!不需要试着说服我相反的......)


Kaz*_*Kaz 6

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*,但只能通过将 的性能拉低letlet*。如果我们将环境链表示为一个简单的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)

表达式12与 无关lambda;他们没有被包围在其中。

所以很明显,如果我们想要一个let与上面相对应的糖表示法,我们必须小心地保留这个属性:

(let ((a 1) (b 2))
  (+ a b))
Run Code Online (Sandbox Code Playgroud)

如果我们希望它let与前面的相同lambda,我们必须使它看起来好像ab是函数参数,并且12是参数表达式。

如果您是一名研究人员,正在使用一种有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)

于是,事情又回到了原点。一开始,有LAMBDALAMBDA产生了LETLETbegatLET*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)


Zor*_*orf 5

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

但最重要的是想法。