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))
,@body))
(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. (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.)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.
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.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
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.
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.