可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I understand the difference between LET and LET* (parallel versus sequential binding), and as a theoretical matter it makes perfect sense. But is there any case where you've ever actually needed LET? In all of my Lisp code that I've looked at recently, you could replace every LET with LET* with no change.
Edit: OK, I understand why some guy invented LET*, presumably as a macro, way back when. My question is, given that LET* exists, is there a reason for LET to stay around? Have you written any actual Lisp code where a LET* would not work as well as a plain LET?
I don't buy the efficiency argument. First, recognizing cases where LET* can be compiled into something as efficient as LET just doesn't seem that hard. Second, there are lots of things in the CL spec that simply don't seem like they were designed around efficiency at all. (When's the last time you saw a LOOP with type declarations? Those are so hard to figure out I've never seen them used.) Before Dick Gabriel's benchmarks of the late 1980's, CL was downright slow.
It looks like this is another case of backwards compatibility: wisely, nobody wanted to risk breaking something as fundamental as LET. Which was my hunch, but it's comforting to hear that nobody has a stupidly-simple case I was missing where LET made a bunch of things ridiculously easier than LET*.
回答1:
LET
itself is not a real primitive in a Functional Programming Language, since it can replaced with LAMBDA
. Like this:
(let ((a1 b1) (a2 b2) ... (an bn))
(some-code a1 a2 ... an))
is similar to
((lambda (a1 a2 ... an)
(some-code a1 a2 ... an))
b1 b2 ... bn)
But
(let* ((a1 b1) (a2 b2) ... (an bn))
(some-code a1 a2 ... an))
is similar to
((lambda (a1)
((lambda (a2)
...
((lambda (an)
(some-code a1 a2 ... an))
bn))
b2))
b1)
You can imagine which is the simpler thing. LET
and not LET*
.
LET
makes code understanding easier. One sees a bunch of bindings and one can read each binding individually without the need to understand the top-down/left-right flow of 'effects' (rebindings). Using LET*
signals to the programmer (the one that reads code) that the bindings are not independent, but there is some kind of top-down flow - which complicates things.
Common Lisp has the rule that the values for the bindings in LET
are computed left to right. Just how the values for a function call are evaluated - left to right. So, LET
is the conceptually simpler statement and it should be used by default.
Types in LOOP
? Are used quite often. There are some primitive forms of type declaration that are easy to remember. Example:
(LOOP FOR i FIXNUM BELOW (TRUNCATE n 2) do (something i))
Above declares the variable i
to be a fixnum
.
Richard P. Gabriel published his book on Lisp benchmarks in 1985 and at that time these benchmarks were also used with non-CL Lisps. Common Lisp itself was brand new in 1985 - the CLtL1 book which described the language had just been published in 1984. No wonder the implementations were not very optimized at that time. The optimizations implemented were basically the same (or less) that the implementations before had (like MacLisp).
But for LET
vs. LET*
the main difference is that code using LET
is easier to understand for humans, since the binding clauses are independent of each other - especially since it is bad style to take advantage of the left to right evaluation (not setting variables as a side effect).
回答2:
You don't need LET, but you normally want it.
LET suggests that you're just doing standard parallel binding with nothing tricky going on. LET* induces restrictions on the compiler and suggests to the user that there's a reason that sequential bindings are needed. In terms of style, LET is better when you don't need the extra restrictions imposed by LET*.
It can be more efficient to use LET than LET* (depending on the compiler, optimizer, etc.):
- parallel bindings can be executed in parallel (but I don't know if any LISP systems actually do this, and the init forms must still be executed sequentially)
- parallel bindings create a single new environment (scope) for all the bindings. Sequential bindings create a new nested environment for every single binding. Parallel bindings use less memory and have faster variable lookup.
(The above bullet points apply to Scheme, another LISP dialect. clisp may differ.)
回答3:
I come bearing contrived examples. Compare the result of this:
(print (let ((c 1))
(let ((c 2)
(a (+ c 1)))
a)))
with the result of running this:
(print (let ((c 1))
(let* ((c 2)
(a (+ c 1)))
a)))
回答4:
In LISP, there's often a desire to use the weakest possible constructs. Some style guides will tell you to use =
rather than eql
when you know the compared items are numeric, for example. The idea is often to specify what you mean rather than program the computer efficiently.
However, there can be actual efficiency improvements in saying only what you mean, and not using stronger constructs. If you have initializations with LET
, they can be executed in parallel, while LET*
initializations have to be executed sequentially. I don't know if any implementations will actually do that, but some may well in the future.
回答5:
I was recently writing a function of two arguments, where the algorithm is expressed most clearly if we know which argument is larger.
(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)
...)
Supposing b
was larger, if we'd used let*
, we would have accidentally set a
and b
to the same value.
回答6:
The main difference in Common List between LET and LET* is that symbols in LET are bound in parallel and in LET* are bound sequentially. Using LET does not allow the init-forms to be executed in parallel nor does it allow the order of the init-forms to be changed. The reason is that Common Lisp allows functions to have side-effects. Therefore, the order of evaluation is important and is always left-to-right within a form. Thus, in LET, the init-forms are evaluated first, left-to-right, then the bindings are created, left-to-right in parallel. In LET*, the init-form is evaluated and then bound to the symbol in sequence, left-to-right.
CLHS: Special Operator LET, LET*
回答7:
i go one step further and use bind that unifies let
, let*
, multiple-value-bind
, destructuring-bind
etc., and it's even extensible.
generally i like using the "weakest construct", but not with let
& friends because they just give noise to the code (subjectivity warning! no need to try convincing me of the opposite...)
回答8:
Presumably by using let
the compiler has more flexibility to reorder the code, perhaps for space or speed improvements.
Stylistically, using parallel bindings shows the intention that the bindings are grouped together; this is sometimes used in retaining dynamic bindings:
(let ((*PRINT-LEVEL* *PRINT-LEVEL*)
(*PRINT-LENGTH* *PRINT-LENGTH*))
(call-functions that muck with the above dynamic variables))
回答9:
(let ((list (cdr list))
(pivot (car list)))
;quicksort
)
Of course, this would work:
(let* ((rest (cdr list))
(pivot (car list)))
;quicksort
)
And this:
(let* ((pivot (car list))
(list (cdr list)))
;quicksort
)
But it's the thought that counts.
回答10:
The OP enquires "ever actually needed LET"?
When Common Lisp was created there was a boat load of existing Lisp code in assorted dialects. The brief the folks who designed Common Lisp accepted was to create a dialect of Lisp that would provide common ground. They "needed" to make it easy and attractive to port existing code into Common Lisp. Leaving LET or LET* out of the language might have served some other virtues, but it would have ignored that key goal.
I use LET in preference to LET* because it tells the reader something about how the data flow is unfolding. In my code, at least, if you see a LET* you know that values bound early will be used in a later binding. Do I "need" to do that, no; but I think it's helpful. That said I've read, rarely, code that defaults to LET* and the appearance of LET signals that the author really wanted it. I.e. for example to swap meaning of two vars.
(let ((good bad)
(bad good)
...)
There is debatable scenario that approaches 'actual need'. It arises with macros. This macro:
(defmacro M1 (a b c)
`(let ((a ,a)
(b ,b)
(c ,c))
(f a b c)))
works better than
(defmacro M2 (a b c)
`(let* ((a ,a)
(b ,b)
(c ,c))
(f a b c)))
since (M2 c b a) isn't going to work out. But those macros are pretty sloppy for assorted reasons; so that undermines the 'actual need' argument.
回答11:
In addition to Rainer Joswig's answer, and from a purist or theoretical point of view. Let & Let* represent two programming paradigms; functional and sequential respectively.
As of to why should I just keep using Let* instead of Let, well, you are taking the fun out of me coming home and thinking in pure functional language, as opposed to sequential language where I spend most of my day working with :)
回答12:
With Let you use parallel binding,
(setq my-pi 3.1415)
(let ((my-pi 3) (old-pi my-pi))
(list my-pi old-pi))
=> (3 3.1415)
And with Let* serial binding,
(setq my-pi 3.1415)
(let* ((my-pi 3) (old-pi my-pi))
(list my-pi old-pi))
=> (3 3)
回答13:
The let
operator introduces a single environment for all the bindings which it specifies. let*
, at least conceptually (and if we ignore declarations for a moment) introduces multiple environments:
That is to say:
(let* (a b c) ...)
is like:
(let (a) (let (b) (let (c) ...)))
So in a sense, let
is more primitive, whereas let*
is a syntactic sugar for writing a cascade of let
-s.
But never mind that. (And I will give justification later below for why we should "never mind"). Fact is there are two operators, and in "99%" of the code, it does not matter which one you use. The reason to prefer let
over let*
is simply that it does not have the *
dangling at the end.
Whenever you have two operators, and one has a *
dangling on its name, use the one without the *
if it works in that situation, to keep your code less ugly.
That is all there is to it.
That being said, I suspect that if let
and let*
exchanged their meanings, it would likely be more rare to use let*
than now. The serial binding behavior will not bother most code that doesn't require it: rarely would the let*
have to be used to request parallel behavior (and such situations could also be fixed by renaming variables to avoid shadowing).
Now for that promised discussion. Although let*
conceptually introduces multiple environments, it's very easy to compile the let*
construct so that a single environment is generated. So even though (t least if we ignore ANSI CL declarations), there is an algebraic equality that a single let*
corresponds to multiple nested let
-s, which makes let
look more primitive, there is no reason t actually expand let*
into let
to compile it, and even a bad idea.
One more thing: note that Common Lisp's lambda
actually uses let*
-like semantics! Example:
(lambda (x &optional (y x) (z (+1 y)) ...)
here, the x
init-form for y
accesses the earlier x
parameter, and similarly (+1 y)
refers to the earlier y
optional. In this area of the language, a clear preference for sequential visibility in binding shows through. It would be less useful if the forms in a lambda
could not see the parameters; an optional parameter couldn't be succinctly defaulted in terms of the value of previous parameters.
回答14:
Under let
, all of the variable initializing expressions see exactly the same lexical environment: that which surrounds the let
. If those expressions happen to capture lexical closures, they can all share the same environment object.
Under let*
, every initializing expression is in a different environment. For each successive expression, the environment must be extended to create a new one. At least in the abstract semantics, if closures are captured, they have different environment objects.
A let*
must be well-optimized to collapse the unnecessary environment extensions in order to suitable as an everyday replacement for let
. There has to be a compiler which works which forms are accessing what and then converts all of the independent ones into larger, combined let
.
(This is true even if let*
is just a macro operator that emits cascaded let
forms; the optimization is done on those cascaded let
s).
You cannot implement let*
as a single naive let
, with hidden variable assignments to do the initializations because the lack of proper scoping will be revealed:
(let* ((a (+ 2 b)) ;; b is visible in surrounding env
(b (+ 3 a)))
forms)
If this is turned into
(let (a b)
(setf a (+ 2 b)
b (+ 3 a))
forms)
it will not work in this case; the inner b
is shadowing the outer b
so we end up adding 2 to nil
. This sort of transformation can be done if we alpha-rename all of these variables. The environment is then nicely flattened:
(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
For that we need to consider the debug support; if the programmer steps into this lexical scope with a debugger, do we want them dealing with #:g01
instead of a
.
So basically, let*
is the complicated construct which has to be optimized well to perform as well as let
in cases when it could reduce to let
.
That alone wouldn't justify favoring let
over let*
. Let's assume we have a good compiler; why not use let*
all the time?
As a general principle, we should favor higher-level constructs that make us productive and reduce mistakes, over error-prone lower-level constructs and rely as much as possible on good implementations of the higher-level constructs so that we rarely have to sacrifice their use for the sake of performance. That's why we are working in a language like Lisp in the first place.
That reasoning doesn't nicely apply to let
versus let*
, because let*
is not clearly a higher level abstraction relative to let
. They are about "equal level". With let*
, you can introduce a bug that is solved by simply switching to let
. And vice versa. let*
really is just a mild syntactic sugar for visually collapsing let
nesting, and not a significant new abstraction.
回答15:
I mostly use LET, unless I specifgically need LET*, but sometimes I write code that explicitly needs LET, usually when doing assorted (usually complicated) defaulting. Unfortunately, I do not have any handy code example at hand.
回答16:
who feels like re-writing letf vs letf* again? the number of unwind-protect calls?
easier to optimize sequential bindings.
maybe it effects the env?
allows continuations with dynamic extent?
sometimes (let (x y z)
(setq z 0
y 1
x (+ (setq x 1)
(prog1
(+ x y)
(setq x (1- x)))))
(values () ))
[ I think that works ] point is, simpler is easier to read sometimes.