News for hackers. Stuff that matters.
Re:Reading a Programmer's Guide to Common Lisp by ogogmad on Thursday February 22, 2024 @09:27AM (#39467532)
I think SETF only reassigns a variable that's already been declared. DEFVAR defines a dynamically-scoped global variable, no? So why doesn't Common Lisp let you write `(VAR new-var new-val)` like every single other language, and have it declare a variable in the current scope? Unless there's a good reason, this is yet another obstacle to these languages being adopted by anybody except the die-hards.

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?

Re:Reading a Programmer's Guide to Common Lisp by martinflack on Thursday February 22, 2024 @12:31PM (#39470289)
You might like Guile, a form of Scheme. It lets you have define calls inside a begin call (progn equivalent) exactly as you ask.
Re:Reading a Programmer's Guide to Common Lisp by lispm on Thursday February 22, 2024 @09:50AM (#39467905)
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)
Re:Reading a Programmer's Guide to Common Lisp by ogogmad on Thursday February 22, 2024 @09:58AM (#39468024)
I 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.

Re:Reading a Programmer's Guide to Common Lisp by reikonomusha on Thursday February 22, 2024 @11:59AM (#39469841)
I 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))
         ,@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)))
Re:Reading a Programmer's Guide to Common Lisp by ogogmad on Thursday February 22, 2024 @02:57PM (#39472226)
One 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.
Re:Reading a Programmer's Guide to Common Lisp by reikonomusha on Thursday February 22, 2024 @03:11PM (#39472458)
If BLOCK weren't already the name of something in Common Lisp, this sort of syntax is very easy to add, easy enough to write in an HN comment. Let's call it OGOGMAD instead.

    (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.)
Re:Reading a Programmer's Guide to Common Lisp by Jtsummers on Thursday February 22, 2024 @03:10PM (#39472431)
It wouldn't require any changes to the language. It would probably require a tree-walking macro, which are not too uncommon. You'd want a different name though since `block` is already a thing in CL.

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.

Re:Reading a Programmer's Guide to Common Lisp by lispm on Thursday February 22, 2024 @05:45PM (#39474387)
Reducing indentation is mostly a non-goal in Lisp. Can't work with nested lists? Don't use Lisp. Lisp processes nested lists. Lisp means List Processor.

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.

Re:Reading a Programmer's Guide to Common Lisp by kazinator on Friday February 23, 2024 @12:18AM (#39477251)
We would specifically not want this macro to be looking inside forms like when, and just analyze the forms that are its direct arguments. (Which is pretty easy.)
Re:Reading a Programmer's Guide to Common Lisp by kazinator on Friday February 23, 2024 @12:47AM (#39477390)
TXR Lisp:

  (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.
Re:Reading a Programmer's Guide to Common Lisp by samatman on Thursday February 22, 2024 @04:28PM (#39473514)
Lua throws an error when you try and do analogous things. I think it's compile time, actually, might be runtime though. It would be reasonable for a language like Common Lisp to reject potentially-undefined variables.
Re:Reading a Programmer's Guide to Common Lisp by kragen on Thursday February 22, 2024 @10:40AM (#39468698)
i'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

Re:Reading a Programmer's Guide to Common Lisp by nanna on Thursday February 22, 2024 @10:21AM (#39468389)
I 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.
Re:Reading a Programmer's Guide to Common Lisp by dannymi on Thursday February 22, 2024 @01:44PM (#39471282)
>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>>
Re:Reading a Programmer's Guide to Common Lisp by ogogmad on Thursday February 22, 2024 @02:40PM (#39471988)
Python 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.

Re:Reading a Programmer's Guide to Common Lisp by dannymi on Thursday February 22, 2024 @03:05PM (#39472363)
> It[Python] certainly does suggest there's compatibility with VAR syntax.

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? :)
Re:Reading a Programmer's Guide to Common Lisp by tome on Thursday February 22, 2024 @05:13PM (#39474055)
> How would you write what I wrote in Lisp in Haskell

    main = do
        let x = 1
        let y = 2
        let z = 3
        let y' = x
            z' = y
            x' = z
        print [x', y', z']
Re:Reading a Programmer's Guide to Common Lisp by kazinator on Thursday February 22, 2024 @09:28PM (#39476215)
It 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.

Re:Reading a Programmer's Guide to Common Lisp by pfdietz on Thursday February 22, 2024 @09:41AM (#39467760)
Common Lisp doesn't have a top level lexical scope, which seems to be what you're looking for here.

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?

Re:Reading a Programmer's Guide to Common Lisp by HexDecOctBin on Thursday February 22, 2024 @09:58AM (#39468033)
Why is this a problem for Common Lisp, and not for the dozens of Scheme implementations that allow you to (define ...) anywhere? (never mind what the RnRS standard says)

> 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.

Re:Reading a Programmer's Guide to Common Lisp by dreamcompiler on Thursday February 22, 2024 @12:18PM (#39470119)
> Instead, CL forces you to do this:

  (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))
Re:Reading a Programmer's Guide to Common Lisp by pfdietz on Thursday February 22, 2024 @11:10AM (#39469189)
Nesting is a general problem beyond let, so it deserves a general solution. A solution in Lisp is the nest macro.

https://fare.livejournal.com/189741.html

Re:Reading a Programmer's Guide to Common Lisp by medo-bear on Thursday February 22, 2024 @10:16AM (#39468307)
> Do you see the problem?

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.

Re:Reading a Programmer's Guide to Common Lisp by HexDecOctBin on Thursday February 22, 2024 @10:32AM (#39468574)
So, every other language that doesn't create a new nested scope for every consecutive group of intermediate variables is doing it wrong? Which is to say, almost every language apart from Common Lisp. Does that sound reasonable to you?
Re:Reading a Programmer's Guide to Common Lisp by kragen on Thursday February 22, 2024 @10:53AM (#39468890)
while i'm not sure lisp's approach is better in this case, i do think it's fairly common that 'almost every language apart from ... lisp' 'is doing it wrong', so i don't think that would be an unreasonable position to hold on those grounds ;)
Re:Reading a Programmer's Guide to Common Lisp by a-french-anon on Thursday February 22, 2024 @11:00AM (#39469016)
You're talking about a special operator, a language primitive. In that view, let is perfectly fine and clear. Macros are what you want if syntactic sugar is needed.
Re:Reading a Programmer's Guide to Common Lisp by medo-bear on Thursday February 22, 2024 @12:47PM (#39470547)
I'd instead point out that an appeal to majority is a basic fallacy.

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.

Re:Reading a Programmer's Guide to Common Lisp by Karrot_Kream on Thursday February 22, 2024 @02:09PM (#39471588)
You've rediscovered another essential portion of Lisp: the smug lisp weenie.
Re:Reading a Programmer's Guide to Common Lisp by kazinator on Thursday February 22, 2024 @05:06PM (#39473977)
> So, every other language that doesn't create a new nested scope for every consecutive group of intermediate variables is doing it wrong?

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; ...

Re:Reading a Programmer's Guide to Common Lisp by shawn_w on Thursday February 22, 2024 @01:33PM (#39471143)
Scheme only allows internal defines at the start of a block, where they're all combined into a letrec (or at least treated the same way). A few do allow define anywhere; SRFI-245, which gives a formal description of the semantics, lists 5 implementations that do, including some popular ones, but it's hardly dozens.

Racket also allows it and encourages it in the style guide, using the "less nesting" (and thus shallower indentation ) rationale you bring up.

https://srfi.schemers.org/srfi-245/srfi-245.html

Re:Reading a Programmer's Guide to Common Lisp by lispm on Thursday February 22, 2024 @04:35PM (#39473595)
> And some Scheme code:

    (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.
Re:Reading a Programmer's Guide to Common Lisp by kazinator on Thursday February 22, 2024 @04:46PM (#39473743)
Since (preamble) cannot possibly perform a side effect which affects the initializing expression 5, it can be moved:

  (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))
Re:Reading a Programmer's Guide to Common Lisp by ogogmad on Thursday February 22, 2024 @09:49AM (#39467892)
Why would a variable declaration be executed conditionally, unless you can somehow GOTO past the declaration? CL doesn't let you do that, does it?

> 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?
Re:Reading a Programmer's Guide to Common Lisp by pfdietz on Thursday February 22, 2024 @11:15AM (#39469246)
One reason to not allow this is that it breaks the equivalence that (for example)

   (progn <form>)
is equivalent to

    <form>
Re:Reading a Programmer's Guide to Common Lisp by martinflack on Thursday February 22, 2024 @12:39PM (#39470415)
^ This is huge. A lot of top-level macro usage would be really annoying to implement if wrapping a PROGN around top-level commands neutered their effect.
Re:Reading a Programmer's Guide to Common Lisp by medo-bear on Thursday February 22, 2024 @10:25AM (#39468444)
> Why would a variable declaration be executed conditionally, unless you can somehow GOTO past the declaration? CL doesn't let you do that, does it?

http://clhs.lisp.se/Body/s_tagbod.htm#tagbody

http://clhs.lisp.se/Body/s_go.htm#go

Re:Reading a Programmer's Guide to Common Lisp by kazinator on Thursday February 22, 2024 @05:44PM (#39474375)
TXR Lisp has a form of unhinged let in the "opip syntax" that underlies all of its threading macros.

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.

Re:Reading a Programmer's Guide to Common Lisp by pfdietz on Friday February 23, 2024 @03:32AM (#39478274)
There's a de facto standard macro for Common Lisp that does something similar, called nest. It's in UIOP.