Can anyone explain the difference simply? I don't think I understand the concept from the textbooks/sites I have consulted.
-
2possible duplicate of [Scheme Confusing of Let and Let\*](http://stackoverflow.com/questions/8036840/scheme-confusing-of-let-and-let) – David Pfeffer Feb 21 '13 at 13:34
-
4@DavidPfeffer - Doesn't seem to be a dupe. That one is asking about a very specific interaction of nested `let`s and `let*`s, while this one is asking for a general overview. – Inaimathi Feb 21 '13 at 15:05
-
simply confused human explanation of machine exectution :o – Nishant Jan 11 '14 at 10:23
2 Answers
Let
is parallel, (kind of; see below) let*
is sequential. Let
translates as
((lambda(a b c) ... body ...)
a-value
b-value
c-value)
but let*
as
((lambda(a)
((lambda(b)
((lambda(c) ... body ...)
c-value))
b-value))
a-value)
and is thus creating nested scope blocks where b-value
expression can refer to a
, and c-value
expression can refer to both b
and a
. a-value
belongs to the outer scope. This is also equivalent to
(let ((a a-value))
(let ((b b-value))
(let ((c c-value))
... body ... )))
There is also letrec
, allowing for recursive bindings, where all variables and expressions belong to one shared scope and can refer to each other (with some caveats pertaining to initialization). It is equivalent either to
(let ((a *undefined*) (b *undefined*) (c *undefined*))
(set! a a-value)
(set! b b-value)
(set! c c-value)
... body ... )
(in Racket, also available as letrec*
in Scheme, since R6RS), or to
(let ((a *undefined*) (b *undefined*) (c *undefined*))
(let ((_x_ a-value) (_y_ b-value) (_z_ c-value)) ; unique identifiers
(set! a _x_)
(set! b _y_)
(set! c _z_)
... body ... ))
(in Scheme).
update: let
does not actually evaluate its value-expressions in parallel, it's just that they are all evaluated in the same initial environment where the let
form appears. This is also clear from the lambda
-based translation: first the value expressions are evaluated each in the same, outer environment, and the resulting values are collected, and only then new locations are created for each id and the values are put each in its location. We can still see the sequentiality if one of value-expressions mutates a storage (i.e. data, like a list or a struct) accessed by a subsequent one.

- 70,110
- 9
- 98
- 181
-
If it's not actually parallel what is the value of ever using `let`? I use it by default since that seems to be the convention other people use and then every time I get a confusing error I change it to `let*` which begs the question, why not always use `let*` to begin with? – Joseph Garvin Oct 03 '21 at 20:04
-
@JosephGarvin `let*` is considered "imperative" in spirit, `let` - declarative. it is _conceptually_ in parallel in that all RHS'es are evaluated in the same environment (the outer one). the only way to see the order in `let`'s initialization is if the init expressions mutate structure, which is frowned upon under the "declarative" paradigm. so if we don't do mutation, it _is_, for us, in parallel, i.e. we needn't concern ourselves with the timing of initializations, as we do with `let*`. the less concern with exact timing the better, the declarative paradigm contends. – Will Ness Oct 03 '21 at 20:42
-
*if they mutate _shared_ structure, that is. if the structure is encapsulated and its mutation isn't observable from the outside, it's OK to mutate it [if needed for efficiency sake](https://stackoverflow.com/a/13256555/849891). – Will Ness Oct 03 '21 at 20:57
If you use let
, you can't reference other bindings which appear in the same let
expression.
For example, this won't work:
(let ((x 10)
(y (+ x 6))) ; error! unbound identifier: x
y)
But if you use let*
, it is possible to refer to previous bindings which appear in the same let*
expression:
(let* ((x 10)
(y (+ x 6))) ; works fine
y)
=> 16
It's all here in the documentation.

- 70,110
- 9
- 98
- 181

- 232,561
- 37
- 312
- 386
-
1I do not see it clearly in the documentation (where your link points, current version 5.3.6), so i was confused too. The documentation for `let` says that "The first form evaluates the `val-exprs` left-to-right, ...", so it is not clear that they are evaluated in parallel. – Alexey Jan 03 '14 at 10:03
-
1@Alexey it does not evaluate them in parallel. As the docs says, *"The first form evaluates the `val-exprs` left-to-right, creates a new location for each `id`, and places the values into the locations"* -- meaning, first they are evaluated and the resulting values are collected, and only ***then*** new locations are created for each *`id`* and the values are put each in its location. You can still see the sequentiality if one of *`val-exprs`* mutates a storage (i.e. data, like list or struct) accessed by a subsequent one. – Will Ness Jan 07 '17 at 14:07
-
Even though this explains the distinction it doesn't really address why the distinction exists in the first place. Every other language I've ever worked with just treats every declaration as a separate sequential binding that can depend on previous bindings. In fact in ALGOL derived languages there is usually no concept of "simultaneous" declarations at all, they are always in some order so it's as if you are always using `let*`. I don't see the downside to always using `let*`, so why does this confusing oddity still exist? Does preserving the distinction allow expressing anything new? – Joseph Garvin Oct 03 '21 at 20:02
-
@JosephGarvin with `let` you can execute the assignments in any order, even in parallel (more aligned with the functional programming way of doing things), whereas `let*` forces an evaluation order and creates a dependency on the previous variable's values (a very procedural programming way of doing things). – Óscar López Oct 03 '21 at 20:10
-
@ÓscarLópez but as I understand it in practice scheme implementations actually never evaluate them in parallel? Also you would be long the problem of needing to infer whether dispatch to other threads would pay for itself. And there’s nothing preventing the expressions being bound from touching the same state so you can’t really be sure parallel execution is safe either. I have to imagine most scheme code in the wild would break if let bindings got randomly reordered. – Joseph Garvin Oct 03 '21 at 20:15
-
@JosephGarvin if you stick to strictly functional programming (no mutation, no side effects) the code wouldn't break, how could it? Agreed, in practice it's not parallel - but it could be, as long as we limit ourselves to the functional subset of the language. One of the tenets of functional programming is that the programmer is relieved of the burden of prescribing flow of control, `let` facilitates this, unlike `let*`. But I'm digressing :) Scheme is not the best language to have this discussion, as is not strictly FP. – Óscar López Oct 03 '21 at 20:22