5

As probably all experienced elispers have found at some point, code like is broken:

(let ((a 3)
      (b 4)
      (c (+ a b)))
  c)

One should use the let* form instead when referring to a just-binded variable within the binding clauses.

I just wonder - why is a seemingly wrong behavior the default? Are there any risks on choosing always let* regardless of how is one gonna use it?

dkim
  • 3,930
  • 1
  • 33
  • 37
deprecated
  • 5,142
  • 3
  • 41
  • 62

4 Answers4

23

Any let* forms can be easily (and mechanically) expressed using let. For example, if we didn't have the let* special form, we could express

(let* ((a 3)
       (b 4)
       (c (+ a b)))
  c)

as:

(let ((a 3))
  (let ((b 4))
    (let ((c (+ a b)))
      c)))

On the other hand, some let forms are quite difficult, if possible at all, to express using let*. The following let form cannot be expressed using let* without introducing an additional temporary variable or esoteric bitwise manipulation.

(let ((x y) (y x)) ; swap x and y
  ...)

So, in this sense, I think let is more fundamental than let*.

dkim
  • 3,930
  • 1
  • 33
  • 37
6

I was taught that the reason is mainly historical, but it might still hold: Since let does parallel assigment, having no data dependencies between the variables might give the compiler flexibility compile for speed or space and compile to something much faster than let*. I don't know how much the elisp compiler uses this.

A bit of googling reveals a similar question for Common Lisp: LET versus LET* in Common Lisp

Community
  • 1
  • 1
joao
  • 3,517
  • 1
  • 31
  • 43
  • +1 for parallel assignment. More important now in multicore environments, although I'm not sure many compilers take advantage of this. – Diego Sevilla Aug 07 '12 at 21:21
  • 2
    I'm not sure about how the Elisp and CL compilers are really doing now, but the Hyperspec, a de facto standard reference for CL, [says differently](http://www.lispworks.com/documentation/lw50/CLHS/Body/s_let_l.htm): "The form `(let ((var1 init-form-1) (var2 init-form-2) ...) ...)` first evaluates the expressions `init-form-1`, `init-form-1`, and so on, **in that order**, saving the resulting values." – dkim Aug 08 '12 at 22:03
  • 1
    Yes, its very subtle. The assigment is done in parallel, but the calculation of expressions is sequential. see this particular answer http://stackoverflow.com/questions/554949/let-versus-let-in-common-lisp/562975#562975. I wonder if this makes the performance-based argument really a myth or there is still some advantage to be taken from this relaxation. – joao Aug 09 '12 at 07:51
  • 1
    This is an example where the sequential order matters: `(let ((y (setq x 1)) (z (setq x 2))) x)`. `let` is not so much free from data dependency. – dkim Aug 09 '12 at 07:51
  • For sure, I fully agree! Thats why I stated the reasons are mainly historical (it might have been different in a an early draft of the CL standard or in a different lisp). I'm asking though is what kind of better code can still the compiler generate for `let` that it *cannot* generate for `let*`. Maybe a different SO question? – joao Aug 09 '12 at 07:55
4

it is incorrect to do so

(let ((a 10)
      (b a)) <- error
  b)

it is correct to do so:

(let* ((a 10)
       (b a))
   b) <- will evaluate to 10.

The error in the first case is due to semantics of let.

let* adds new symbols to the existing environment, evaluating them inside it.

let creates a new environment, evaluating the new symbols in the current envinronemnt, and the new environment will be disponible at the end of the evaluation of all new symbols, to evaluate the code of (let () code ).

let* is syntactic sugar for

(let ((a xxx))
  (let ((b a)) ...)

while let is syntactic sugar for

((lambda (a)
  ...)
  val_a)

The second form is much more common, so perhaps they thought to give it a shorter name...

alinsoar
  • 15,386
  • 4
  • 57
  • 74
2

I'm not sure I would call let broken. For instance, you may for whatever reason want to shadow a and b in the enclosed environment, while defining c with respect to the enclosing expression.

(let ((a 11)
      (b 12))
  (let ((a 3)
        (b 4)
        (c (+ a b)))
    ;; do stuff here will shadowed `a' and `b'
    c))
> 23

As for risks, I don't know if there are any per se. But why created multiple nested environments when you don't have to? There may be a performance penalty for doing so (I don't have enough experience with elisp to say).

Jason Morgan
  • 2,260
  • 21
  • 24