5 ms·
Programming languages may let you define variables by assigning them. But the scope can differ across languages. Common Lisp requires one to clearly define the
by lispm 3y ago
Programming languages may let you define variables by assigning them. But the scope can differ across languages.
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)
- ogogmad 3y agoI know what the difference is between LET and LET*. What you haven't provided is a motivation for forcing the programmer to worry about that. It seems there's no concrete example of where unstarred LET would be better. 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. 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.
- nanna 3y agoI think that the distinction makes the code clearer to read. LET tells a future reader that they don't need to bother scanning each variable binding for parent variables, whereas LET* tells them that they do. Seems like the same logic behind having a WHEN and UNLESS as opposed to just an IF, the former meaning that one needn't search for an 'else' block. The LISP family encourage good style.
- kragen 3y agoi'm not really sure myself, but i can think of some minor advantages 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 binding with `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
- reikonomusha 3y agoI think you're overstating a supposed worry. I could equally say, "why are all of these languages making me worry about which scope a VAR is attached to?" In either case, it doesn't seem to actually be a worry, it's just different than what you're used to day-to-day. 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)))
- ogogmad 3y agoOne possible alternative is to combine PROGN with LET, which you might then call BLOCK, with the macro definition of BLOCK looking for `VAR`s. The VAR syntax wouldn't make sense anywhere else. More exactly: (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.
- dannymi 3y ago>It seems there's no concrete example of where unstarred LET would be better. 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>>
- ogogmad 3y agoPython expresses destructuring bind using: 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.
- kazinator 3y agoIt is historic. Originally there was only lambda. Local variables could be obtained using lambda terms, and immediately calling them with arguments. The variables in a lambda receive the evaluated argument values, and come into existence simultaneously. let was derived from lambda and understood in terms of those fixed arguments, and inherited that semantics. 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.