132 comments

[ 2.8 ms ] story [ 208 ms ] thread
One place I get stuck all the time with Common Lisp is the REPL. I'm used to IPython, which allows me to enter multiple lines and execute them all at once, then go "up" and get all those lines back and change something to fix the error:

    In [1]: my_list = [1, 2, 3]
       ...: my_other_list = [4, 5, 6]
       ...: final_list = [str(i) for i in my_list ++ my_other_list] # whoops a syntax error!
       ...:
       ...: "".join(final_list)
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    Cell In[1], line 3
          1 my_list = [1, 2, 3]
          2 my_other_list = [4, 5, 6]
    ----> 3 final_list = [str(i) for i in my_list ++ my_other_list] # whoops a syntax error!
          5 "".join(final_list)

    TypeError: bad operand type for unary +: 'list'

    In [2]: my_list = [1, 2, 3]
       ...: my_other_list = [4, 5, 6]
       ...: final_list = [str(i) for i in my_list + my_other_list]
       ...:
       ...: "".join(final_list)
    Out[2]: '123456'

All the CL REPLs I've tried only allowing getting back one line at a time, which feels tedious to execute in order. I feel like I'm fundamentally missing something about iterative development in Common Lisp and it's blocking me from learning the language.
When I last used CL using Emacs+SLIME helped a lot. Especially C-x C-e (evaluate under the cursor) and C-M-x (evaluate form you are inside of). With this you get all the benefits of classic editor together with REPL-like instant feedback.

https://slime.common-lisp.dev/

In a Lisp-aware editor, such as Emacs with SLIME, you can send to the REPL for evaluation an arbitrary block of text that contains multiple separate expressions. See for example M-x slime-eval-region (C-c C-r ).
When I read “I feel like I'm fundamentally missing something about iterative development in Common Lisp” in the GP, I thought of exactly what’s in these replies. I’ve only recently started learning CL via Practical Common Lisp, and while I liked Emacs+SLIME, I’m a vim guy (I know) and switched to vim+VLIME instead, and so far I’m loving it. This to me has actually been the “secret sauce” of Lisp in my early experience, because now when I go to write code or use the REPL for languages like Python and Ruby, I find myself missing the SLIME/VLIME experience. I find it to be a very intuitive and efficient way to write code interactively.
Even VS has send to REPL kind of experience for Python, maybe vim isn't the right tool, rather using something else.
For the record you can do the same with various emacs python modes, and it is vastly superior to using the python repl alone.
They typically read one expression at a time. Wrap your multiline/multiexpression code in a let or progn expression or similar if you want to write multiple expressions before evaluating them.

They will normally let you write as many lines as needed for an expression and then let you go back and edit the whole thing.

If you want something like IPython, you need to look into the commercial survivors from Common Lisp, with a full blown IDE experience, like Lisp Works and Allegro Common Lisp.
Are you using the CL REPL directly on a terminal emulator? I do not type expressions directly into the REPL often. I do sometimes but not often. Instead I send expressions right from the editor to the REPL. The REPL itself is available as a separate buffer in the editor. How this is done depends on the editor/IDE. Some examples:

• With Emacs + SLIME, we can type C-M-x (evaluate current top-level form) or C-x C-e (evaluate the expression before the cursor).

• With Vim + Slimv, it is ,e and ,d respectively.

• Yet another popular option is Vim + Vlime in which case the key sequences are \ss and \st instead.

I have written two guides to explain how to create such a development environment from scratch and get started with such a setup:

https://github.com/susam/emacs4cl

https://susam.net/lisp-in-vim.html

There are commercial implementation + IDEs too which according to many people provide even better integrated development experience. The most popular among them is perhaps LispWorks. I use Emacs + SLIME myself.

Yes, I was directly typing in the REPL. I think Slimv may have been the thing I'm missing. When I'm back to my laptop I will give it a shot. Thanks!
I would type into a REPL, but not in a terminal. Typically one would run a REPL inside an editor or something similar. The tool to run a REPL is usually called a "Listener" in Lisp. For example I would use GNU Emacs, call SLIME and use a SLIME REPL.

Lisp development in a terminal, without an editor, is not so helpful.

It's been a week so I can't edit my other response to this, but yes, Slimv was what I was missing. I read your second link and followed the instructions and am now able to actually learn this language. Thank you for sharing.

For anyone else that knows Vim but can't seem to get a good CL environment going, read https://susam.net/lisp-in-vim.html

In Lisp you'd do something like

    (let* ((my-list '(1 2 3))
           (my-other-list '(4 5 6))
           (final-list (concatenate 'list my-list my-other-list)))
       (whatever-cl-calls-join final-list ""))
This is a single Lisp form evaluated at the REPL that you could get back and edit as necessary. It's been a decade or so haha but that's the idea
Broad question about Lisp conventions: Why can't you just define each variable in a separate line, instead of batching those definitions up at the top? This seems like a confusing break from convention. In C-derived languages, you can stick variable definitions anywhere.

I've looked it up and Common Lisp doesn't seem to provide such an option. Seems like an odd restriction. Oh, and the difference between LET, LET* and LETREC is another weird break from convention.

You can do each variable on its own line with stuff like "defvar" and "setf", but that will add them to the current package namespace - and convention is to avoid polluting namespaces with names that will be unused later. So, if the variables are only used within a given scope, the scope is explicitly delineated with a "let" block. It's not a general requirement.

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

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?

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?
One reason to not allow this is that it breaks the equivalence that (for example)

   (progn <form>)
is equivalent to

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

(comment deleted)
> 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.

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

You've rediscovered another essential portion of Lisp: the smug lisp weenie.
> 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; ...

> 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))
(comment deleted)
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

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

There's a de facto standard macro for Common Lisp that does something similar, called nest. It's in UIOP.
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)
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.

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

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

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

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

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

You might like Guile, a form of Scheme. It lets you have define calls inside a begin call (progn equivalent) exactly as you ask.
i'm not sure it's correct to say that lisp's 01959 approach to defining variables is a 'confusing break from conventions' established by c, which originated in 01972 but didn't support 'sticking variable definitions anywhere' until 01999 with the 01999 revision of the iso c standard. i would rather say that sticking variable declarations anywhere, a feature i believe was introduced to the mainstream by c++ around 01990, was the break from conventions. almost all the languages i'm familiar with from before that required you to batch up local variable declarations at the tops of scopes: c, smalltalk, pascal, scheme

the exception is basic, where you could defint i or dim x(128) at any point, but it would be an understatement to say that basic's conventions were not widely emulated by other languages

also basic variables are global. most of these languages let you create global variables anywhere, or at least anywhere outside of a subroutine
I hope this doesn't derail the conversation much, but I found it interesting that you seem to be using 5 digit years, a convention I've never seen before. What drives this choice?
it turns out it's sort of like wearing a mohawk. a long now mohawk. it's a completely harmless deviation from convention which provokes an amazing variety of reactions, some astonishingly aggressive, while other people react thoughtfully
Thank you. I hadn't drawn the parallel before. But that matches my experience with both.
> In C-derived languages, you can stick variable definitions anywhere.

ANSI C89 requires that variable declarations only occur at the beginning of the scope.

Mixed declarations and statements appeared in C99. C90 didn't have them.

Other Algol-like languages including Algol itself also separate declarations and statements: Pascal, Modula, Ada, ...

In C90, you can have variables anywhere, but they must be wrapped in a compound statement. This has a number of advantages:

1. You can repeat variables:

   { 
     int yes = 1;
     setsockopt(fd, SOL_SOCKET, SO_WHATEVER, &yes);
   }

   { 
     int yes = 1;
     setsockopt(fd, SOL_SOCKET, SO_OTHER, &yes);
   }
2. You can move code around more easily, and move these sub-blocks into their own functions more easily, since they follow "declaration close to use".

3. You control the end of the scope of the variables, not only the beginning. You know that after each of the above two closing braces, the yes variable is no longer in scope. (If there is a yes variable in scope, it must be coming from a parent scope; it is not "leaking" sideways.)

4. If you write a goto which goes around these encapsulated scopes, that goto does not jump into a region where the variable is uninitialized. Compare:

   void fn()
   {
      goto end;
      { 
         int x = 42;
      }
      end: ;

      // x is not in scope here, OK
   }


   void fn()
   {
      goto end;
      int x = 42;
      end: ;
      // x is in scope here, but not initialized!
   }
For these reasons, I avoid mixing declarations and statements in C programming; the separation is a very good idea. It's enforced in Ada, where it is rationalized with safety arguments. Mixing declarations and statements encourages bugs. It lets you hack dubious solutions into the program without having to think about how its structure could be improved to do it cleanly.

In Common Lisp and similar dialects, you can mix imperative code with variable definitions by using progn.

  (let ((x (get-x-object)
        (y (progn (perform-side-effect)
                  (get-y-object)))
        (z (get-z-object)))
    ...)

On a small number of occasions, I've done the above in C, using the comma operator, to avoid mixed declarations and statements:

  int x = get_x();
  int y = (perform_side_effect(), get_y());
It's a little dirty but not as dirty as mixed declarations and statements.

Also note that there is no need for mixed declarations and statements, if your code is functional! In any of your code that emphasizes functional programming, you should not run into a need for this, because the entire purpose of the feature is to be able to stick a side effect between variable definitions, which has to be sequenced there.

The Scheme language has the feature of defining variables anywhere. You can use the define form in any scope, like inside a function:

  (define (fun arg)
    (define x 0)
    (define y 1)
    ...)
How this works is that Scheme implementations perform a code walk which transforms these defines into nested let forms. Each sequential body of expressions has to be scanned for the presence of define and treated this way.

It's pretty ugly; you end up with what looks like a self-contained form (define x 0) that is frobbing the surrounding lexical scope, such that another form not enclosed in it depends on its definition of x.

In Common Lisp and related dialects, anything that starts with "(def" is understood to be for top-level definitions only, which work by performing a run-time side effect when they are evaluated. (And so their effect is visible to later forms not due to lexical scope but due to the chronological order of execution.)

There is a strong metaprogramming advantage in having rigid variable binding forms like let. Because let has all the variables in one place, it is easy to interpret and compile. This is one of the things that helps a Lisp-in-Lisp metacircular evaluator be very short. If you have variables d...

> If you write a goto which goes around these encapsulated scopes, that goto does not jump into a region where the variable is uninitialized.

That's true when you write a goto that goes around scopes, and doesn't jump into them. However, you can still jump into the middle of an inner scope, where sometimes a variable might be uninitialized.

  void c89_fn(int arg) {
      if (arg > 0)
          goto after;
      { 
          int x = 42; /* or "int x; x = 42;" */
  after:
          /* x is in scope, but x could be uninitialized */
          printf("%d\n", x);
      }
  }
For this particular example, GCC and Clang inline the definition of x as 42 unconditionally with any amount of optimizations on, but with -O0, both generate code that reads from a sometimes-uninitialized stack address. There's probably a more practical example that one could find.
The point is that your goto cannot avoid this, if your local variable scopes close at the end of the function, due to not using nested blocks. If you jump around those variables, you're jumping into a region where they are still visible and not inited.

Also, my original example is invalid C++, as is yours.

C++ does not allow goto to skip variable initializations.

In C++, if you want to goto past some variable declarations, you have to do the right thing and encapsulate them into a block.

GCC's diagnostics look like:

  goto.cc: In function ‘void f()’:
  goto.cc:7:1: error: jump to label ‘end’ [-fpermissive]
   end:;
   ^~~
  goto.cc:3:8: note:   from here
     goto end;
        ^~~
  goto.cc:5:7: note:   crosses initialization of ‘int x’
     int x = 42;
         ^
> The point is that your goto cannot avoid this, if your local variable scopes close at the end of the function, due to not using nested blocks. If you jump around those variables, you're jumping into a region where they are still visible and not inited.

I see. I think I get what you're saying: if you only put variable declarations at the top of the scope, then if you only jump to labels at the same statement depth as the goto statement, you won't skip initialization or assignment of uninitialized variables. Whereas, if you mix declarations and statements, you might end up with uninitialized variables even when you only jump to labels at the same statement depth as the goto statement. Is that what you're saying, or did I grasp something orthogonal to your point?

> C++ does not allow goto to skip variable initializations.

I didn't know that until now. However, if you change the function so that x is assigned after being declared, it still compiles and produces the same result.

  void c89_fn(int arg) {
      if (arg > 0)
          goto after;
      { 
          int x;
          x = 42;
  after:
          /* x is in scope, but x could be uninitialized */
          printf("%d\n", x);
      }
  }
Lisp existed before any of the conventions of languages like C were invented.
Typically the convention is to introduce a new scope with let. You can, if you really want to, declare the names at the top and then assign values later, similar to old C, though this is not idiomatic:

  (let (foo bar baz)
    (setf foo 2)
    (other-code)
    (setf bar (expt foo 2))
    (more-code)
    (setf baz 5))
the correct answer as far as LET vs LET* will be lost to all the noise, but here it goes. people are just defending a historic artifact like it actually makes any kind of sense, or like it was a deliberate design choice. it's not and it wasn't, and you're totally correct as far as it being confusing. common lisp is compromise between a variety of lisp vendors, who all were working on variously mutated dialects of the same language in the 80s, while the languages was evolving from the 60s! much of the languages functionality was added while people were figuring out how to do high level programming, so constructs that seem fundamental to us weren't introduced until much later in the process, because nobody knew that such a thing could exist.

if you read documents from that era, the code that they write is wildly alien, because present day programming wasn't invented yet. things that you take for granted just didn't exist. common lisp and to a lesser extent scheme carry all that baggage for backwards compatibility.

so when lisp came out, it didn't have LET, and LET didn't appear until mid 70s. and it was added as a convenience macro for ((lambda (VAR1 VAR2 ...) ...) FORM1 FORM2 ...), which is how people did dynamic binding (also called lambda-binding) for a decade prior. evolution of lisp claims that LET came from lisp machine lisp, but people were independently inventing it all over the place, as a custom macro. depending on how your lambdas were evaluated or how your LET macro was written, you're not just assigning VARs simultaneously, you might not even have a guarantee of the order of evaluation of FORMs. but it got its job done, which is according to revised scheme manual "allowing the forms for the quantities to appear textually adjacent to their corresponding variables". this was a novel convenience at some point!

LET* was invented after LET as a convenience for LET, because sequential binding was even more convenient than binding in general. it would've made sense to then make LET* the "default" of some sort, but the subtle distinction stuck for reasons of legacy code, writing conventions, acquired preferences, and then it got crystalized and preserved for posterity in the common lisp standard.

> you might not even have a guarantee of the order of evaluation of FORMs.

However, a let based on lambda would have parallel binding. The evaluation of the forms would mainly come from the argument evaluation order of the lambda, where the macro would have to go out of its way to screw it up.

It's worth noting that Common Lisp has optional parameters. These use sequential binding like let*. So we could translate

  (let* ((a (a)) (b (b))) c)
into

  (funcall (lambda (&optional (a (a)) (b (b)))))
The lambda is called with no arguments, so that the defaulting takes place, and that has all the semantics we need. (We could also similarly exploit &aux).

The fact that CL's optional parameters use sequential binding kind of shows that it's the preferred mode.

The reason that the fixed parameters of lambda have parallel binding is that the values don't come from the lambda form itself, but from the arguments, which are already evaluated. So there is no way for a parameter value to be calculated from another parameter value. They come into existence at the same time.

Not so with optionals; they have default expressions, and those can refer to the prior variables.

Thus let came from lambda, and was understood in terms of fixed, required parameters. Required parameters come into the scope simultaneously, and so let variables came into scope simultaneously.

revised report on scheme which introduced LET to scheme at the time still didn't guarantee left-to-right order of evaluation, "the argument forms can in principle be evaluated in any order. this is unlike the usual LISP left-to-right order". so when introducing LET, which is defined in terms of LAMBDA, the report reiterates that order of evaluation is not guaranteed.
Yes, to this day, I think, Scheme doesn't require a particular order for the evaluation of function arguments, like C.
By the way, the original author of CLISP (a free Common Lisp implementation) liked mixed variables and statements so much that the C sources use a .d rather than .c suffix and are preprocessed to .c files through a text processing script called "varbrace" which turns mixed declarations and statements into C90.

That was long before C99; the project started around the middle 1980s, I think.

CLISP might benefit from being updated to C99, with all the files renamed to .c, and varbrace eliminated, but I don't think anyone's gotten around to it.

Eh, it'd be more like...

  (let ((my-list '(1 2 3))
        (my-other-list '(4 5 6)))
    (map 'String #'write-to-string
         (append my-list my-other-lisp)))
No need for 'join the list to an empty string to convert it' shenanigans.
This works:

  (let ((my-list '(1 2 3))
               (my-other-list '(4 5 6)))
    (format nil "~{~A~}"
     (append my-list my-other-list)))
The sibling comments seem to interpret your need in a lispy way! In IPython, you could use arrow keys to go back in history and then edit the code you want and execute it. It seems you're saying that CL doesn't have this feature. But what the commenters say is you don't probably need this feature anyway because you can write the code you want in the editor and send it to REPL. Python's REPL has this feature too, but obviously it's not a "clean" approach because sometimes we just want to experiment with things and don't want to pollute the main code with these one-off expressions.
i think it would be more accurate to gloss the sibling comments as saying that the place where lisps put this feature is in emacs rather than in ipython; that is, emacs is the lisp equivalent of ipython

python's built-in repl also lacks the desired feature, and it's kind of a pain in the ass, but the ^o keybinding can go some distance to compensating for it; when you use ↑ or ^r to get back to a desired line in history that begins a multiline block, after editing it, type ^o instead of enter, and the next line will appear below. works in bash too, and it's super common in my experience to want to run multiple historical commands in sequence instead of just one

jupyter notebook is maybe a better alternative to the emacs feature. darius bacon's halp provides a sort of notebook-like feature in emacs

I kind of figured I was approaching this wrong. If that approach was useful, I would have found someone already implemented it. One silly hangup I have is I am more comfortable in Vim than Emacs, and I am trying to isolate my learning to a language and not a language-plus-an-editor. This must be how people feel when I suggest they switch to Vim to make a certain workflow easier :-)

My goal isn't really to get "IPythonButForCommonLisp", but to iteratively build up programs so I can quickly learn syntax. I'm going to go with the Slimv suggestion the next chance I get and see if that solves it. Instead of working in revisioned code, I'll probably just work in `Untitled.lisp` until I get the hang of things.

So, I use the CL repl in emacs pretty heavily (SLIME) and M-p / M-n do the thing you’re talking about ipython doing. You can also use the arrow keys to navigate to a previous expression and hit enter to copy the whole expression to the current input.
Well, just begin your typing with `(progn ` and when you’ve typed the lines you want just press `)`.

And/or run your repl inside emacs and you’ll have your whole history available right there.

I learned a trick from Clojure that helps with this (they call them rich comments).

You never type into the REPL but include comment sections across your programs where you write code as it's intended to be used/executed and then use key bindings to highlight/run in your REPL.

This allows you write several lines and highlight them in order to run them.

CLISP uses READLINE, and it's "form" based, so if you have a multi-line form, an up-arrow, you get the entire form.

SBCL doesn't have anything that I'm aware off, neither does CCL.

I've seen mentioned of wrapping SBCL in a readline wrapper. There's a program that essentially gives readline behavior to anything that reads stdin, a readline interface. It may be name something clever like "readline", I've forgotten. I've never used it.

The terminal experience is weak on those Lisps simply because of the dominance emacs has in this space. Wrap SBCL or CCL in Slime and you get readline and more. There's simply little demand for a more functional CLI when the emacs/slime combo is so powerful and useful.

The burden for Slime and emacs (for this use case) is actually quite low. Both are pretty easy to install, modern emacs out of the box works with simple mouse gestures and arrow keys, so you don't need to be an emacs wonk to use it. And Slime has its own dropdown menu for most tasks.

Readline in CLISP is useful, it makes CLISP orders of magnitude more useable than raw SBCL. Cutting and pasting S-Exprs is just not a great experience for routine work, IMHO. One advantage of readline over the emacs buffers is that when you up-arrow, you get the form. In the buffer, if you up-arrow you go up one line. Mildly annoying when your last form spat out a 1000 lines. (That's why you search instead, but, nit noted.) With readline your REPL experience is more like the shells.

And that may all work with the readline wrapper, but the wrapper may well not be aware of S-exprs, so if you enter a multi line expression and up-arrow you may get just the last line of your last expression. Kind of worst of both world. But, I'm just supposing here, I've not used it.

I'm quite content with CLISP (which does not have a lot of modern activity on it), I just wish I could get it with SSL. This seems to be some grand challenge I have not found a top-of-first-page "SSL in Clisp" solution for on google.

Anyway, enough rambling.

Install emacs and slime.

Lisp is not "line oriented", but "expression oriented". Lisp is a "List Processor", not a "Text Processor". If you want to group expressions in a REPL into one expression, use PROGN (or similar).

The R in REPL stands for READ, which is a Lisp function, which reads an expression and returns data.

I love it that Paolo says that Medley is his preferred environment. I try Medley periodically, and for me in modern times, Emacs with SBCL or LispWorks fits my needs better.

I was fortunate enough to have had a Xerox 1108 Lisp Machine purchased for me in 1982. I loved it with InterLisp-D but a few years later I started running it in Common Lisp mode, and the 1.5 megabytes of RAM in my 1108 was not really adequate.

In any case, the Medley developers make it easy to try Medley so give it a try.

> Emacs with SBCL or LispWorks fits my needs better.

Do you use both, rather than one predominantly?

I usually use Emacs with a console save of LispWorks Pro, sometimes Emacs+SBCL, sometimes the LispWorks Pro IDE.

EDIT: the reason why I usually use a LispWorks console save instead of SBCL is obscure: I often ingest very large text files, and the last time I checked a few years ago, LW was faster at this than SBCL. For regular CL hacking, SBCL is fine.

Thanks. Although it's not ANSI Common Lisp compliant and misses modern niceties, I love a Lisp Machine environment like Medley because it's a self-contained, self-sufficient, coherent computing universe. A rich space for my personal projects and explorations.
Have you ever tried Mathematica? It's commercial, but probably the closest thing to what you're talking about. It has something like 5000+ built-in functions for everything from calculus to neural networks, charting, image manipulation, geographic mapping, videos, symbolic computing, File I/O, matrix math, optimization...etc etc. The language itself is a term-rewriting system that is conceptually similar to lisp in a lot of ways.
One of the main differences is that much of Mathematica is written in C++ (for example the UI). The Mathematica language then itself is a slow language (term rewriting, is not a really good low-level language).

Medley is written is largely written in Interlisp (and a bit of Common Lisp), including its UI. Interlisp (originally as BBN Lisp) was originally developed as an integrated development environment with complete source management (similar what Smalltalk later did). In the 70s it was then moved to the metal on early workstations (again, similar what Smalltalk did) -> it was its own OS. It does memory management, networking, graphics, all in Interlisp, ... Its purpose was to be a development environment for Lisp (here Interlisp).

Mathematica's purpose is to be an integrated tool in the mathematics domains, including applied mathematics in physics, chemistry, visualization, biology, ...

The nearest system to Interlisp-D is/was Smalltalk 80.

Oh I agree that a Lisp machine or Smalltalk machine is turtles all the way down. The only problem is that it seems like there isn't really a good modern option with fully modern libraries and all that. Mathematica is a nice tool for exploring all kinds of computations in a manner that is kind of similar to lisp with brackets if you squint. That may or may not be of use to some on here. I only mention it as I don't think many in the CS field get exposed to it.
> I only mention it as I don't think many in the CS field get exposed to it.

Because it is a specialized commercial offering.

> Oh I agree that a Lisp machine or Smalltalk machine is turtles all the way down.

Many non-Lisp-Machine Lisps are also mostly written in itself. Implementations like SBCL provide a wide spectrum of performance.

I'm sure Mathematica is great even if I never tried it. But, to me, a large part of the appeal of a Lisp Machine environment is Lisp itself. I mean the real thing with parantheses and all, not a language inspired by, derived from, or related to Lisp. With no other language I achieved the fluency and naturalness I have with Lisp.
I agree. I took a very brief look at Medley and SBCL+emacs is working well for me.
I love picking up old technical books. They always seem to have a perspective that's lacking in more recent books (not that recent books don't _also_ have a useful perspective, just a different one). The sort of information the author assumes, or doesn't assume, conveys as much as the topic of the book itself.

I've been trying to work through The Little Schemer myself lately in the same vein as the poster. It's tough going, honestly, but so far I think it's been worth it.

My guilty pleasure has been going through the books at thrift stores and reading the old technical manuals/guides/etc.

The biggest thing with the older ones is the lack of assumption of Internet, so the book will refer to itself instead of referring to online/other documentation.

I've learned things reading a "Missing Manual" for an operating system that is 15 years out of date that still work today.

> The biggest thing with the older ones is the lack of assumption of Internet, so the book will refer to itself instead of referring to online/other documentation.

I also miss this aspect of old books. They were more self-contained.

I feel I can get a quite detailed understanding of DOS, Windows 3.11, even Windows 95 from printed books and reference materials of the time.

But I feel much of the documentation/reference around things from 2010 is both late enough that it wasn't printed, and old enough that what was online has failed or faded away.

It helps that stable versions lasted a lot longer back then, and things moved around a lot less.

Nowadays if you need to change a setting, you not only have to contend with major version but what biannual revision of windows you are using, and hope that someone at Microsoft hasn't decided to move that option in an update.

Even the documentation that is still around is disorganized and useless. I want a linear path I can follow to learn something, not some random collection of hyperlinks to more information.
Another great thing about books is that they have a built in order to them. Start at page 1 and read the pages in order until the end. You can skip around, and they provide some aid for that via the table of contents and the index and references in the text to other parts of the text, but they (usually) are designed so that if you just start at the beginning and read through to the end you get everything the book has to offer in an order that makes sense.

Compare to far too much online documentation. This is what I've frequently run into. I want to learn about some particular subject, so I find a site that has an introduction or tutorial on that. The document is even book-like in the sense that it is organized as pages, and at the bottom of each page there are "next" and "previous" links.

But there is a sidebar on each page, with lists of related material and some of that sounds like material I'm going to need to know at some point. But there is no indication if it is material that I'm going to come across later if I just keep following the "next" links or it is something beyond the scope of the current document that I'll need to bookmark now to come back to later.

> and read the pages in order until the end

And if you put it down and pick it back up tomorrow, it will still be where you left off.

I love old books too. One I really really enjoyed is Wirth's "Algorithms + Data Structures = Programs". It's so well written, and beautifully typeset and bound. Just a lovely little artifact.
Yes! One of my university textbooks and I still remember what a nice physical artifact it was.
I loved reading TAOCP. I don’t think I’ve ever seen anything as _carefully_ laid out as that series was/is.
And I thought I was the only one with this hobby of buying and reading old computer programming books.
I must admit, though, that I buy way more of them than I read.
I went on an archaeological tear a few years ago and acquired every book that I could find that talked about higher-level assembly language programming, such as XPL/S, "A systems Implementation Language for the Xerox Sigma Computers" and "Machine Oriented Higher Level Languages".
The Little Schemer is one of my favorite books. Really delightful.
"The Little Schemer" is one of the best programming books ever.

I have used it to teach scheme/lisp to people who would never learn "programming". It's just that good.

I think some technical writers in the past could also write novels (i.e. Jerry Pournelle) and the transfer of ability and knowledge would go both ways. So when you read a manual like the one for the Jupiter Ace, it is an absolute joy.

"You may well be wondering by this stage why the computer isn't taking any notice of all this rubbish you've typed in. The reason is not that it's already noticed it's rubbish, but simply that it hasn't looked yet. It won't take any notice until you press what is just about the most important key on the keyboard, the one marked ENTER (on the right-hand side, one row up)."

http://jupiter-ace.co.uk/downloads/JA-Manual-First-Edition-[...

One of my favourites is "The AWK Programming Language". Very concisely written, very natural to follow, and very insightful even if you do not care about AWK at all.

https://awk.dev/

I second this. Awk is one of my favorite programming languages (I would even say, one of my favorite tools) and "The AWK Programming Language" is definitely a worthy book.
You both will like this gopher hole:

gopher://hoi.st

It has virtual machines implemented in awk, some generic awk library and a good 'phlog' to read great posts on unix, minimalism and several tools and games. The freecell game it's a easy example.

That brings back memories. I remember I had the original AWK book...it came with a 5.25" floppy disc with PolyAwk ("The Toolbox Language") inside.
GNU awk has internet/socket support so you can write a gopher/irc/finger client in few lines.
I will likely never be paid to work with it, nor will I ever justify working with it, but I still have a VB6 'COMPLETE' book I am probably never throwing away, it is a lot of fun to go through it over the years and look back at how things used to be. I wish VB had retained its ability to build native apps instead of becoming a .NET language that eventually got dropped. It was a fun first-time programming language, and I'm sure there's plenty of apps still coded in VB6 out in production right now...
TLS is a very rewarding book. I get back to the ideas therein often. While most things become natural after a while, I find that technique of building function to collect more than one value at a time sometimes very useful.
I have had this book for many years. I remember the toy expert system ("Otto") and enjoyed learning from the example. If I remember correctly the author makes good use of CLOS in the book.
Read "Common Lisp: A Gentle Introduction to Symbolic Computation" , much better. And funnier. Later, "Paradigms of Artificial Intelligence Programming".
I used to have a bunch of old technical books. My university's library would give them away, and I'd pick them up for novelty.

I ended up getting rid of most of them a while back, since I carried them with me from move to move. Now that I'm in a place that I'll be in for a long time, I really miss these books.

The coolest one I saw, but never picked up, was a book entirely dedicated to creating a chess engine. It was published years ago at the time I saw it (seen 2013, published in 1980?), but I doubt the basics have changed all that much.

The art and graphics in old books are great as well. I always like to think of older albums, books, and movies as a snapshot in time. It's fun to see a snapshot in time in such a specific niche.

For those who have the book, what is contained in the "discussion of the interactive Lisp programming process" as mentioned by Paolo?
The discussion basically says you don't follow an edit the full program-compile-run cycle like in other languages with batch compilers. Instead, you interactively create a program by writing individual expressions (e.g. function definitions) in the editor, sending them to the REPL for evaluation and testing, further experimenting at the REPL, and repeating the cycle. If you're already familiar with Lisp environments there's nothing new in the book.