views:

1867

answers:

11

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*.

+4  A: 

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.

David Thornley
Good point. Though since Lisp is such a high-level language, that just makes me wonder why "weakest possible constructs" is such a desired style in Lisp land. You don't see Perl programmers saying "well, we don't *need* to use a regexp here..." :-)
Ken
I don't know, but there's a definite style preference. It's opposed somewhat by people (like me) who like to use the same form as much as possible (I almost never write setq instead of setf). It may have something to do with an idea to say what you mean.
David Thornley
+1  A: 

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))
Doug Currie
+7  A: 

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)))
Logan Capaldo
Care to develop why this is the case?
John McAleely
@John: in the first example, `a`'s binding refers to the outer value of `c`. In the second example, where `let*` allows bindings to refer to previous bindings, `a`'s binding refers to the inner value of `c`. Logan isn't lying about this being a contrived example, and it doesn't even pretend to be useful. Also, the indentation is nonstandard and misleading. In both, `a`'s binding should be one space over, to line up with `c`'s, and the 'body' of the inner `let` should be just two spaces in from the `let` itself.
zem
+10  A: 

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

Mr Fooz
+4  A: 

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

Attila Lendvai
Ooh, neat. I'm going to go play with BIND now. Thanks for the link!
Ken
+2  A: 

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 LET*, the init-form is evaluated and then bound to the symbol in sequence, left-to-right.

CLHS: Special Operator LET, LET*

tmh
A: 

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.

Vatine
+6  A: 

LET itself is not a real primitive in a functional 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.

Gabriel published his book on 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).

Rainer Joswig
A: 
(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.

Lajla
A: 

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

beidy
A: 

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