I'm not sure what you mean about the difference between LET and LET* (the latter simply lets subsequent variable declarations refer to previously declared variables in the same block), and LETREC is not a builtin part of Common Lisp.
I'm not sure what you mean about the difference between LET and LET* (the latter simply lets subsequent variable declarations refer to previously declared variables in the same block), and LETREC is not a builtin part of Common Lisp.
If you want to carefully delimit scope, doesn't Common Lisp (and other Lisps) provide PROGN? Seems like a less complicated approach.
> I'm not sure what you mean about the difference between LET and LET* (the latter simply lets subsequent variable declarations refer to previously declared variables in the same block
Why does LET even exist as an alternative to LET*? Why does Lisp even bother making this distinction?
Putting something like VAR everywhere now causes issues because it's not a form that returns a value. Also, there's no place to hang declare forms on it. And if it's executed conditionally, what does that mean? The var is declared on one branch but not the other?
> top level lexical scope
What does this mean? Every single other language lets you write:
var x = y;
Why is Lisp seemingly the only (imperative) language that doesn't? (progn <form>)
is equivalent to <form>> And if it's executed conditionally, what does that mean? The var is declared on one branch but not the other?
Here is some C code:
if (cond()) {
preamble();
int a = 5;
do_stuff(a);
} else {
do_stuff(6);
}
And some Scheme code: (if (cond)
(begin
(preamble)
(define a 5)
(do-stuff a))
(do-stuff 6))
So why can't Common Lisp have the equivalent? (if (cond)
(progn
(preamble)
(var a 5)
(do-stuff a))
(do-stuff 6))
Instead, CL forces you to do this: (if (cond)
(progn
(preamble)
(let ((a 5))
(do-stuff a)))
(do-stuff 6))
Do you see the problem? Common Lisp wants you to declare every set of intermediate variables in a nested scope, leading to super-deep nesting unless you start breaking you function into small pieces for no reason (which then hurts readability).This is why languages die, when the old guard refuses to see that there are better ways of doing things than what they are used to.
No. Explicit scoping is a huge plus.
> This is why languages die, when the old guard refuses to see that there are better ways of doing things than what they are used to.
Don't mistake this for not being able to see beyond your nose.
edit:
Let me also add this: you cannot get away from scoping. If you create intermediate values in your language of choice you better understand the implicit (and sometimes very complicated) scoping rules of that language. All that Common Lisp does is it makes this scoping explicit.
In my opinion, it is a giant, flaming misfeature.
> Which is to say, almost every language apart from Common Lisp
Algol; Ada; Modula 1, 2, and 3; Oberon; Eiffel; ...
(if (cond)
(progn
(preamble)
(let ((a 5))
(do-stuff a)))
(do-stuff 6))
Above would be unusual. I'd only write code that way if preamble was dependent on some previous binding of a. Note that let subsumes progn in most cases.
I'd write it like this: (if (cond)
(let ((a 5))
(preamble)
(do-stuff a))
(do-stuff 6))Racket also allows it and encourages it in the style guide, using the "less nesting" (and thus shallower indentation ) rationale you bring up.
(if (cond)
(begin
(preamble)
(define a 5)
(do-stuff a))
(do-stuff 6))
This is not defining a local variable in Scheme. The various Scheme standards also require that DEFINE appears at the top of a body.> Common Lisp wants you to declare every set of intermediate variables in a nested scope
Lisp programmers edit code by list operations. For example, I can set the cursor between (preamble) and the (let ...) form. control-meta-t transposes the lists. The let form is moved upwards and the enclosed body is moved, too. Try that in the C code. Code transformations are vastly easier with explicit scopes.
JavaScript has introduced a LET for a reason: block scope is clearer than VAR (function / global scope).
(defun foo (a)
(if a
(var b a))
(print b)) ; what's the value here?
In Common Lisp above would not be valid.I would need to write:
(defun foo (a)
(let (b)
(if a
(setf b a))
(print b)))
I can then immediately see that each B is inside a LET scope, which defines it. It's just by simply moving upwards in the expression tree. I would not need to search the whole expression tree. Also languages may do different things with definitions inside conditional expressions. (if (cond)
(let ((a 5))
(preamble)
(do-stuff a))
(do-stuff 6))
You can work in side effects into the expressions of a let or let*: (let* ((x (progn (widget-lock w)
(widget-x w)))
(y ...))
(...)
(widget-unlock w))
If lock-widget returns the widget, it can look like this: (let* ((w (widget-lock w))
(x (widget-x x))
(y ...))
...
(widget-unlock w))https://www.nongnu.org/txr/txr-manpage.html#S-F2CAF1CB
For instance
(flow 1
(+ 2)
(let x) ;; x is 3 now
(+ 3)
(let y) ;; y is 6
(+ 4) ;; pipeline value is 10 now
(+ x y)) ;; 19 returned: (+ 3 6 10)
There is a (let (var1 init1) (var2 init2) ...) syntax supported also. Both these variants act as pass-through pipe elements: they bind variables that are in scope of the rest of the pipe.However, if you use the normal (let ((var init) ...) ...) syntax, then that is not special any more; it is just the regular let being threaded, like any other operator. It does not bind variables visible to the rest of the pipe.
Outside of this, there are only let and let* which resemble the Common Lisp ones.
Note that this construct makes sense even in pure code; it was not introduced for the sake of inserting side effects between variable definitions, but for capturing the output at specific points of the pipe, binding it to a name.
Common Lisp requires one to clearly define the scope. PROGN does not create a scope.
> Why does LET even exist as an alternative to LET*? Why does Lisp even bother making this distinction?
Because there is a scope difference.
One can always do
(let (a b c)
(setf a 10)
...
(setf b 20)
...
(setf c (+ a b)
...)
Think of (let* ((a 10) (b 20) (c (+ a b)))
...)
as a short form for ((lambda (a b)
((lambda (c)
...)
(+ a b)))
10 20)Even Scheme gives this distinction an odd prominence that's not found outside the Lisp family. It seems reasonable that when I write code, I shouldn't have to stop and worry about which of the gazillion different LET forms is appropriate, especially in a high-level language which is supposed to help me write code (or read code) without worrying about irrelevant details like that.
with `let` you can say
(let ((x y) (y x)) ...
without worrying about the ordering of the variable bindings. this is more interesting for dynamically-scoped variables; in emacs lisp, for example, you might want to say (let ((case-fold-search t) (outer-case-fold-search case-fold-search))
...
(let ((case-fold-search outer-case-fold-search)) (f))
...)
so that when you call `f` it doesn't see your case-fold-search bindingwith `let*` you implicitly have an execution sequence over the binding forms, but in most cases that's something you're specifying by accident. `let` strongly suggests to the reader that she can consider any one of the binding forms in the list in isolation; she doesn't have to read through the first n-1 bindings to understand the nth one
it's true that, in most cases, all three of them do the same thing, and this is not the most parsimonious approach
Lisp's design, I find, is best understood through the lens of how we might most straightforwardly interpret the semantics of the syntax, i.e., how we might write an evaluator for the language. It's a large part of the appeal of using Lisp; it's possible to understand the language—from parsing to execution—so well that you could in principle write a conforming interpreter for even something like Common Lisp without Herculean effort. Having such an understanding of the language has a variety of practical benefits even if you're just using the language.
A construct that you suggest like
(VAR <variable> <value>)
would be quite laborious to nail down semantically, especially if we want VAR to work in as many contexts as possible. This goes against the above ethos.Even then, if we do make a variety of decisions about syntax and scoping rules to allow VAR, we have additional questions to answer in the context of Lisp specifically. For example, are macros allowed to expand into VAR statements? Is this allowed?
(defmacro bind ()
'(var x 5))
(defun f (y)
(bind)
(+ x y))
This sort of shenanigan can't happen with LET quite as opaquely, since the closest equivalent would require the sum to be wrapped: (defmacro bind (&body b)
`(let ((x 5))
,@b))
(defun f (y)
(bind
(+ x y)))
Now I'm clued in to something goofy potentially happening when looking at the definition of F.So we might ban macros expanding into VAR statements. So then how do we write binding macros? Introduce a dedicated SCOPE special operator?
(defmacro bind (&body b)
`(scope
(var x 5)
,@b))
(defun f (y)
(bind
(+ x y)))
But now we just have an obscure LET. :)Languages with very complicated grammars also typically come with comparatively complicated scoping rules (e.g., Python). Lisp's LET maybe seems gratuitous in a world of "var x = 2" syntax, but it's also abundantly clear what it means and how it works at all times, regardless of nesting or context.
To leave the reader with one last thing to ponder: What should this do in a compiled implementation of Common Lisp with VAR?
(defun what? ()
(tagbody
(if (= 0 (random 2))
(go :bind)
(go :print))
:bind
(var x 1)
:print
(print x))) (block
(var first-thing 'EXAMPLE)
(do-something)
(var foo 'EXAMPLE)
(do-something-else))
Expand this into: (let ((first-thing 'EXAMPLE))
(do-something)
(let ((foo 'EXAMPLE'))
(do-something-else)))
This would reduce indentation. But it would also involve big changes to Common Lisp.http://clhs.lisp.se/Body/s_block.htm
I'd recommend On Lisp by Paul Graham and Let Over Lambda by Doug Hoyt if you're interested in trying it.
(defmacro ogogmad (&body b)
(cond
((endp b) '(progn))
((endp (rest b)) b)
(t
(let ((f (first b))
(r (rest b)))
(if (eq 'var (first f))
`(let (,(rest f))
(ogogmad ,@r))
`(progn
,f
(ogogmad ,@r))))))
Exactly as you specify, this only lets you put VAR syntax immediately inside of OGOGMAD forms. (Untested, typed on mobile.)You may want to do it in a text oriented language, but not so much in an expression oriented language, where programmers edit an expression tree, by tree manipulation commands.
In Lisp the "tree" is "nested lists".
Btw., what does
(block
(when (foo)
(var first-thing 'EXAMPLE))
(do-something)
(var foo 'EXAMPLE)
(do-something-else))
mean? Is it legal? Your macro would need to traverse the expression tree, knowing the whole Lisp syntax, including expanding macros, possibly in a source interpreter. WHEN is a macro. BLOCK would need to find the VAR expression inside the WHEN macro, which is not easy for the general case. This would be very very strange. Macros are usually expanded outside to inside. Your BLOCK macro would need to expand macros in enclosed code, to find VAR expressions.In JavaScript something like above is legal in a function.
(defmacro ogogmad (:match)
(() ())
(((var @var @init))
^(progn ,init nil))
((@single)
single)
(((var @var @init) . @rest)
^(let ((,var ,init)) (ogogmad ,*rest)))
((@first . @rest)
^(progn ,first (ogogmad ,*rest))))
1> (expand '(ogogmad (var a 4) (var b 5) (+ a b)))
(let ((a 4))
(let ((b 5))
(+ a b)))
2> (expand '(ogogmad))
nil
3> (expand '(ogogmad 42))
42
4> (expand '(ogogmad (var a 42)))
nil
5> (expand '(ogogmad (var a (print 42))))
(progn (print 42)
())
6> (ogogmad (var a 4) (var b 5) (+ a b))
9
:match is a new kind of macro, called a parameter list macro, that I invented. defmacro doesn't know anything about pattern matching. Parameter list macros are expanded in any function/lambda or macro parameter list situation. The :match macro assumes that the function's body consists of pattern matching clauses. It infers the parameters from it, which get substituted.The unstarred let is a destructuring bind of an entire tuple.
For example (let ((x 1) (y 2) (z 3)) ...) does the entirety of x := 1, y := 2, z := 3 at once, the right hand sides in the old frame and the left hand sides in the new frame.
So let introduces a new frame BUT only after all three substitutions are done.
For example in order to rotate x y z (from the surrounding frame) to the right:
(let ((y x) (z y) (x z))
...)
means: ((lambda (y z x)
...)
x y z)
For example: (let ((x 1) (y 2) (z 3))
(let ((y x) (z y) (x z))
(list x y z)))
=> (3 1 2)
The starred let is the weird form. It's shorthand for having another let in the tail each time: With A, B, C each standing for a form: (let* (A B C) ...) is defined to be (let (A) (let* (B C) ...). And so on. (let* (C) ...) is (let (C) ...), the base case.
(let* (A B C) ...) is (let (A) (let (B) (let (C) ...)))). Does that look natural to you?
(let (_) 5) is valid too.>If a programmer ever falls in the habit of sometimes using unstarred LET, then it's likely he'll make a mistake by using it where starred LET* was the right thing.
Never happened to me so far and I'm using Lisp for 8 years now.
>It seems reasonable that when I write code, I shouldn't have to stop and worry about which of the gazillion different LET forms is appropriate
You do you, but you seem to think that this is an aesthetic choice rather than what the mathematics automatically gives. Lisp was not really designed, it was discovered. As long as you don't break any of the mathematical properties, go ahead and make it like you want it to be.
One thing you could safely do is remove the automatic tuple destructuring on let, basically removing the ability to have parallel-track bindings. Then you'd end up with something like Haskell:
main = let x = 1
y = 2
z = 3
in let y = x
z = y
x = z
in x
=> <<loop>> x, y = foo, bar
It occasionally comes in handy. It certainly does suggest there's compatibility with VAR syntax.Haskell's `do` notation also supports destructuring bind the Python way. It's even made into a special case of pattern matching. It also allows you to make declarations anywhere inside a block. This seems better than the Common Lisp approach.
Maybe. Maybe not.
>It[Haskell, or do notation] also allows you to make declarations anywhere inside a block.
main = do
let x = 1
let y = 2
let z = 3
let y = x
z = y
x = z
print x
=> compilation error[1]
Do you find that normal?How would you write what I wrote in Lisp in Haskell (for example using what you said)? Is it gonna need new concepts?
Compare:
main = do
let x = 1
let y = 2
let z = 3
let y = x
let z = y
let x = z
print [x, y, z]
=> [1, 1, 1] (as expected--but it's still dumb)
[1] a.hs:9:5: error:
• Ambiguous type variable ‘a0’ arising from a use of ‘print’
prevents the constraint ‘(Show a0)’ from being solved.
Probable fix: use a type annotation to specify what ‘a0’ should be.
These potential instances exist:
instance Show Ordering -- Defined in ‘GHC.Show’
instance Show a => Show (Maybe a) -- Defined in ‘GHC.Show’
instance Show Integer -- Defined in ‘GHC.Show’
instance Show () -- Defined in ‘GHC.Show’
instance (Show a, Show b) => Show (a, b) -- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c) => Show (a, b, c)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d) => Show (a, b, c, d)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e) =>
Show (a, b, c, d, e)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f) =>
Show (a, b, c, d, e, f)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f,
Show g) =>
Show (a, b, c, d, e, f, g)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g,
Show h) =>
Show (a, b, c, d, e, f, g, h)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g,
Show h, Show i) =>
Show (a, b, c, d, e, f, g, h, i)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g,
Show h, Show i, Show j) =>
Show (a, b, c, d, e, f, g, h, i, j)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g,
Show h, Show i, Show j, Show k) =>
Show (a, b, c, d, e, f, g, h, i, j, k)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g,
Show h, Show i, Show j, Show k, Show l) =>
Show (a, b, c, d, e, f, g, h, i, j, k, l)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g,
Show h, Show i, Show j, Show k, Show l, Show m) =>
Show (a, b, c, d, e, f, g, h, i, j, k, l, m)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g,
Show h, Show i, Show j, Show k, Show l, Show m, Show n) =>
Show (a, b, c, d, e, f, g, h, i, j, k, l, m, n)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g,
Show h, Show i, Show j, Show k, Show l, Show m, Show n, Show o) =>
Show (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o)
-- Defined in ‘GHC.Show’
instance Show a => Show (Solo a) -- Defined in ‘GHC.Show’
instance Show Bool -- Defined in ‘GHC.Show’
instance Show Char -- Defined in ‘GHC.Show’
instance Show Double -- Defined in ‘GHC.Float’
instance Show Float -- Defined in ‘GHC.Float’
instance Show Int -- Defined in ‘GHC.Show’
instance Show Word -- Defined in ‘GHC.Show’
instance Show a => Show [a] -- Defined in ‘GHC.Show’
...plus 13 instances involving out-of-scope types
instance Show GHC.Stack.Types.CallStack -- Defined in ‘GHC.Show’
instance Show GHC.Types.KindRep -- Defined in ‘GHC.Show’
instance Show GHC.Types.Levity -- Defined in ‘GHC.Show’
instance Show GHC.Types.Module -- Defined in ‘GHC.Show’
instance Show GHC.Num.Natural.Natural -- Defined in ‘GHC.Show’
instance Show a => Show (GHC.Base.NonEmpty a)
-- Defined in ‘GHC.Show’
instance Show GHC.Types.RuntimeRep -- Defined in ‘GHC.Show’
instance Show GHC.Stack.Types.SrcLoc -- Defined in ‘GHC.Show’
instance Show GHC.Types.TrName -- Defined in ‘GHC.Show’
instance Show GHC.Types.TyCon -- Defined in ‘GHC.Show’
instance Show GHC.Types.TypeLitSort -- Defined in ‘GHC.Show’
instance Show GHC.Types.VecCount -- Defined in ‘GHC.Show’
instance Show GHC.Types.VecElem -- Defined in ‘GHC.Show’
• In a stmt of a 'do' block: print x
In the expression:
do let x = 1
let y = 2
let z = 3
let y = x
z = y
....
....
In an equation for ‘main’:
main
= do let x = ...
let y = ...
let z = ...
....
|
9 | print x
| ^^^^^
^ Are you sure you want this in your language? :) main = do
let x = 1
let y = 2
let z = 3
let y' = x
z' = y
x' = z
print [x', y', z']let* is not easily understood in terms of a single lambda. Given
(let* ((a1 (expr1))
(a2 (expr2 a1)))
body)
It cannot be that (expr1) and (expr2 a1) are argument expressions in a call to a single lambda, whose parameters are a1 and a2.It can be readily understood as two nested lambdas. And that view makes it appear as if let is the primitive.
That view is not the only possible one; we can also regard let and let* as being an independent lexical binding construct not understood in terms of a lambda reference model. Under that view, the difference between them is very minor. In a compiler, the difference between let and let* can be handled by a few simple conditionals in the compilation strategy.
Be that as it may, let was there first and so the sequential binding let* gets the star. Some people might prefer the sequential construct to be let an the parallel one to be let*, but that's not how it played out.
In Common Lisp, lambda has features not based on original lambda: optional parameters with init expressions, and &aux variables. These behave like let*!
Common Lisp's let* cannot be regarded as expanding into a nested let, because it supports (declare ...), and declarations can apply to all variables. Well, in theory I suppose it's possible to break out a let* with declarations into a nested let, but the procedure for that has to analyze the declarations and separate them by variable, synthesizing new declarations inserted into the let nesting a the appropriate level.