News for hackers. Stuff that matters.
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.