Reading "A Programmer's Guide to Common Lisp"
journal.paoloamoroso.com
journal.paoloamoroso.com
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.
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.
I also miss this aspect of old books. They were more self-contained.
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.
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.
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 if you put it down and pick it back up tomorrow, it will still be where you left off.
I have used it to teach scheme/lisp to people who would never learn "programming". It's just that good.
"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-[...
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.
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.
Do you use both, rather than one predominantly?
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.
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.
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.
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.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.
• 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.
Lisp development in a terminal, without an editor, is not so helpful.
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
(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 ideaI'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.
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.
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?
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?
> 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? (progn <form>)
is equivalent to <form>> 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.
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.
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.
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; ...
(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))Racket also allows it and encourages it in the style guide, using the "less nesting" (and thus shallower indentation ) rationale you bring up.
(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. (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))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.
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)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.
with `let` you can say
(let ((x y) (y x)) ...
without worrying about the ordering of the variable bindings. this is more interesting for dynamically-scoped variables; in emacs lisp, for example, you might want to say (let ((case-fold-search t) (outer-case-fold-search case-fold-search))
...
(let ((case-fold-search outer-case-fold-search)) (f))
...)
so that when you call `f` it doesn't see your case-fold-search bindingwith `let*` you implicitly have an execution sequence over the binding forms, but in most cases that's something you're specifying by accident. `let` strongly suggests to the reader that she can consider any one of the binding forms in the list in isolation; she doesn't have to read through the first n-1 bindings to understand the nth one
it's true that, in most cases, all three of them do the same thing, and this is not the most parsimonious approach
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))) (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.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.
(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.)You may want to do it in a text oriented language, but not so much in an expression oriented language, where programmers edit an expression tree, by tree manipulation commands.
In Lisp the "tree" is "nested lists".
Btw., what does
(block
(when (foo)
(var first-thing 'EXAMPLE))
(do-something)
(var foo 'EXAMPLE)
(do-something-else))
mean? Is it legal? Your macro would need to traverse the expression tree, knowing the whole Lisp syntax, including expanding macros, possibly in a source interpreter. WHEN is a macro. BLOCK would need to find the VAR expression inside the WHEN macro, which is not easy for the general case. This would be very very strange. Macros are usually expanded outside to inside. Your BLOCK macro would need to expand macros in enclosed code, to find VAR expressions.In JavaScript something like above is legal in a function.
(defmacro ogogmad (:match)
(() ())
(((var @var @init))
^(progn ,init nil))
((@single)
single)
(((var @var @init) . @rest)
^(let ((,var ,init)) (ogogmad ,*rest)))
((@first . @rest)
^(progn ,first (ogogmad ,*rest))))
1> (expand '(ogogmad (var a 4) (var b 5) (+ a b)))
(let ((a 4))
(let ((b 5))
(+ a b)))
2> (expand '(ogogmad))
nil
3> (expand '(ogogmad 42))
42
4> (expand '(ogogmad (var a 42)))
nil
5> (expand '(ogogmad (var a (print 42))))
(progn (print 42)
())
6> (ogogmad (var a 4) (var b 5) (+ a b))
9
:match is a new kind of macro, called a parameter list macro, that I invented. defmacro doesn't know anything about pattern matching. Parameter list macros are expanded in any function/lambda or macro parameter list situation. The :match macro assumes that the function's body consists of pattern matching clauses. It infers the parameters from it, which get substituted.The unstarred let is a destructuring bind of an entire tuple.
For example (let ((x 1) (y 2) (z 3)) ...) does the entirety of x := 1, y := 2, z := 3 at once, the right hand sides in the old frame and the left hand sides in the new frame.
So let introduces a new frame BUT only after all three substitutions are done.
For example in order to rotate x y z (from the surrounding frame) to the right:
(let ((y x) (z y) (x z))
...)
means: ((lambda (y z x)
...)
x y z)
For example: (let ((x 1) (y 2) (z 3))
(let ((y x) (z y) (x z))
(list x y z)))
=> (3 1 2)
The starred let is the weird form. It's shorthand for having another let in the tail each time: With A, B, C each standing for a form: (let* (A B C) ...) is defined to be (let (A) (let* (B C) ...). And so on. (let* (C) ...) is (let (C) ...), the base case.
(let* (A B C) ...) is (let (A) (let (B) (let (C) ...)))). Does that look natural to you?
(let (_) 5) is valid too.>If a programmer ever falls in the habit of sometimes using unstarred LET, then it's likely he'll make a mistake by using it where starred LET* was the right thing.
Never happened to me so far and I'm using Lisp for 8 years now.
>It seems reasonable that when I write code, I shouldn't have to stop and worry about which of the gazillion different LET forms is appropriate
You do you, but you seem to think that this is an aesthetic choice rather than what the mathematics automatically gives. Lisp was not really designed, it was discovered. As long as you don't break any of the mathematical properties, go ahead and make it like you want it to be.
One thing you could safely do is remove the automatic tuple destructuring on let, basically removing the ability to have parallel-track bindings. Then you'd end up with something like Haskell:
main = let x = 1
y = 2
z = 3
in let y = x
z = y
x = z
in x
=> <<loop>> x, y = foo, bar
It occasionally comes in handy. It certainly does suggest there's compatibility with VAR syntax.Haskell's `do` notation also supports destructuring bind the Python way. It's even made into a special case of pattern matching. It also allows you to make declarations anywhere inside a block. This seems better than the Common Lisp approach.
Maybe. Maybe not.
>It[Haskell, or do notation] also allows you to make declarations anywhere inside a block.
main = do
let x = 1
let y = 2
let z = 3
let y = x
z = y
x = z
print x
=> compilation error[1]
Do you find that normal?How would you write what I wrote in Lisp in Haskell (for example using what you said)? Is it gonna need new concepts?
Compare:
main = do
let x = 1
let y = 2
let z = 3
let y = x
let z = y
let x = z
print [x, y, z]
=> [1, 1, 1] (as expected--but it's still dumb)
[1] a.hs:9:5: error:
• Ambiguous type variable ‘a0’ arising from a use of ‘print’
prevents the constraint ‘(Show a0)’ from being solved.
Probable fix: use a type annotation to specify what ‘a0’ should be.
These potential instances exist:
instance Show Ordering -- Defined in ‘GHC.Show’
instance Show a => Show (Maybe a) -- Defined in ‘GHC.Show’
instance Show Integer -- Defined in ‘GHC.Show’
instance Show () -- Defined in ‘GHC.Show’
instance (Show a, Show b) => Show (a, b) -- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c) => Show (a, b, c)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d) => Show (a, b, c, d)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e) =>
Show (a, b, c, d, e)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f) =>
Show (a, b, c, d, e, f)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f,
Show g) =>
Show (a, b, c, d, e, f, g)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g,
Show h) =>
Show (a, b, c, d, e, f, g, h)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g,
Show h, Show i) =>
Show (a, b, c, d, e, f, g, h, i)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g,
Show h, Show i, Show j) =>
Show (a, b, c, d, e, f, g, h, i, j)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g,
Show h, Show i, Show j, Show k) =>
Show (a, b, c, d, e, f, g, h, i, j, k)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g,
Show h, Show i, Show j, Show k, Show l) =>
Show (a, b, c, d, e, f, g, h, i, j, k, l)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g,
Show h, Show i, Show j, Show k, Show l, Show m) =>
Show (a, b, c, d, e, f, g, h, i, j, k, l, m)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g,
Show h, Show i, Show j, Show k, Show l, Show m, Show n) =>
Show (a, b, c, d, e, f, g, h, i, j, k, l, m, n)
-- Defined in ‘GHC.Show’
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g,
Show h, Show i, Show j, Show k, Show l, Show m, Show n, Show o) =>
Show (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o)
-- Defined in ‘GHC.Show’
instance Show a => Show (Solo a) -- Defined in ‘GHC.Show’
instance Show Bool -- Defined in ‘GHC.Show’
instance Show Char -- Defined in ‘GHC.Show’
instance Show Double -- Defined in ‘GHC.Float’
instance Show Float -- Defined in ‘GHC.Float’
instance Show Int -- Defined in ‘GHC.Show’
instance Show Word -- Defined in ‘GHC.Show’
instance Show a => Show [a] -- Defined in ‘GHC.Show’
...plus 13 instances involving out-of-scope types
instance Show GHC.Stack.Types.CallStack -- Defined in ‘GHC.Show’
instance Show GHC.Types.KindRep -- Defined in ‘GHC.Show’
instance Show GHC.Types.Levity -- Defined in ‘GHC.Show’
instance Show GHC.Types.Module -- Defined in ‘GHC.Show’
instance Show GHC.Num.Natural.Natural -- Defined in ‘GHC.Show’
instance Show a => Show (GHC.Base.NonEmpty a)
-- Defined in ‘GHC.Show’
instance Show GHC.Types.RuntimeRep -- Defined in ‘GHC.Show’
instance Show GHC.Stack.Types.SrcLoc -- Defined in ‘GHC.Show’
instance Show GHC.Types.TrName -- Defined in ‘GHC.Show’
instance Show GHC.Types.TyCon -- Defined in ‘GHC.Show’
instance Show GHC.Types.TypeLitSort -- Defined in ‘GHC.Show’
instance Show GHC.Types.VecCount -- Defined in ‘GHC.Show’
instance Show GHC.Types.VecElem -- Defined in ‘GHC.Show’
• In a stmt of a 'do' block: print x
In the expression:
do let x = 1
let y = 2
let z = 3
let y = x
z = y
....
....
In an equation for ‘main’:
main
= do let x = ...
let y = ...
let z = ...
....
|
9 | print x
| ^^^^^
^ Are you sure you want this in your language? :) main = do
let x = 1
let y = 2
let z = 3
let y' = x
z' = y
x' = z
print [x', y', z']let* is not easily understood in terms of a single lambda. Given
(let* ((a1 (expr1))
(a2 (expr2 a1)))
body)
It cannot be that (expr1) and (expr2 a1) are argument expressions in a call to a single lambda, whose parameters are a1 and a2.It can be readily understood as two nested lambdas. And that view makes it appear as if let is the primitive.
That view is not the only possible one; we can also regard let and let* as being an independent lexical binding construct not understood in terms of a lambda reference model. Under that view, the difference between them is very minor. In a compiler, the difference between let and let* can be handled by a few simple conditionals in the compilation strategy.
Be that as it may, let was there first and so the sequential binding let* gets the star. Some people might prefer the sequential construct to be let an the parallel one to be let*, but that's not how it played out.
In Common Lisp, lambda has features not based on original lambda: optional parameters with init expressions, and &aux variables. These behave like let*!
Common Lisp's let* cannot be regarded as expanding into a nested let, because it supports (declare ...), and declarations can apply to all variables. Well, in theory I suppose it's possible to break out a let* with declarations into a nested let, but the procedure for that has to analyze the declarations and separate them by variable, synthesizing new declarations inserted into the let nesting a the appropriate level.
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
ANSI C89 requires that variable declarations only occur at the beginning of the scope.
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 defined anywhere, it means that every form that contains statement-like forms must be scanned for those definitions. They are not in a fixed place in the AST: (let vars form ...). That is one big reason why if you implement Scheme's define, you want that to be expanded early, so that everything downstream just sees nice nested lets.
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.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;
^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);
}
} (let (foo bar baz)
(setf foo 2)
(other-code)
(setf bar (expt foo 2))
(more-code)
(setf baz 5))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.
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.
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.
(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. (let ((my-list '(1 2 3))
(my-other-list '(4 5 6)))
(format nil "~{~A~}"
(append my-list my-other-list)))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
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.
And/or run your repl inside emacs and you’ll have your whole history available right there.
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.
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.
The R in REPL stands for READ, which is a Lisp function, which reads an expression and returns data.
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.
https://www.amazon.com/Object-Oriented-Programming-COMMON-LI...