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
在概念上是比较简单的,它应该默认使用。
LOOP
types ? 经常使用。 有一些原始的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)。
但是对于LET
和LET*
,主要区别在于使用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*
,我们会不小心将a
和b
设置为相同的值。
我更进一步,使用绑定 ,统一让,让*,多值绑定,解构绑定等,它甚至是可扩展的。
一般我喜欢用“最薄弱的结构”,但是不要和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()))
[我认为作品]的一点是,简单易读有时候。