[HN Gopher] Reading "A Programmer's Guide to Common Lisp"
___________________________________________________________________
Reading "A Programmer's Guide to Common Lisp"
Author : Tomte
Score : 163 points
Date : 2024-02-22 13:00 UTC (10 hours ago)
(HTM) web link (journal.paoloamoroso.com)
(TXT) w3m dump (journal.paoloamoroso.com)
| ryan-duve wrote:
| 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.
| smatija wrote:
| 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/
| pamoroso wrote:
| 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 ).
| collinrapp wrote:
| 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.
| Jtsummers wrote:
| 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.
| pjmlp wrote:
| 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.
| susam wrote:
| 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.
| ryan-duve wrote:
| 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!
| lispm wrote:
| 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.
| neutronicus wrote:
| 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
| ogogmad wrote:
| 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.
| dwringer wrote:
| 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.
| ogogmad wrote:
| 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?
| pfdietz wrote:
| 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?
| ogogmad wrote:
| 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?
| medo-bear wrote:
| > Why would a variable declaration be executed
| conditionally, unless you can somehow GOTO past the
| declaration? CL doesn't let you do that, does it?
|
| http://clhs.lisp.se/Body/s_tagbod.htm#tagbody
|
| http://clhs.lisp.se/Body/s_go.htm#go
| pfdietz wrote:
| One reason to not allow this is that it breaks the
| equivalence that (for example) (progn
| <form>)
|
| is equivalent to <form>
| martinflack wrote:
| ^ 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.
| HexDecOctBin wrote:
| 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.
| medo-bear wrote:
| > 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.
| HexDecOctBin wrote:
| 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?
| kragen wrote:
| 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 ;)
| a-french-anon wrote:
| 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.
| medo-bear wrote:
| 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.
| Karrot_Kream wrote:
| You've rediscovered another essential portion of Lisp:
| the smug lisp weenie.
| kazinator wrote:
| > _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; ...
| pfdietz wrote:
| Nesting is a general problem beyond let, so it deserves a
| general solution. A solution in Lisp is the nest macro.
|
| https://fare.livejournal.com/189741.html
| dreamcompiler wrote:
| > 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))
| shawn_w wrote:
| 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
| lispm wrote:
| > 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.
| kazinator wrote:
| 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))
| kazinator wrote:
| 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.
| lispm wrote:
| Programming languages may let you define variables by
| assigning them. But the scope can differ across
| languages.
|
| Common Lisp requires one to clearly define the scope.
| PROGN does not create a scope.
|
| > Why does LET even exist as an alternative to LET*? Why
| does Lisp even bother making this distinction?
|
| Because there is a scope difference.
|
| One can always do (let (a b c)
| (setf a 10) ... (setf b 20)
| ... (setf c (+ a b) ...)
|
| Think of (let* ((a 10) (b 20) (c (+ a
| b))) ...)
|
| as a short form for ((lambda (a b)
| ((lambda (c) ...) (+ a b)))
| 10 20)
| ogogmad wrote:
| 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.
| nanna wrote:
| 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.
| kragen wrote:
| 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
| reikonomusha wrote:
| 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)))
| ogogmad wrote:
| 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.
| Jtsummers wrote:
| 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.
| reikonomusha wrote:
| 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.)
| lispm wrote:
| 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.
| samatman wrote:
| 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.
| dannymi wrote:
| >It seems there's no concrete example of where unstarred
| LET would be better.
|
| The unstarred let is a destructuring bind of an entire
| tuple.
|
| For example (let ((x 1) (y 2) (z 3)) ...) does the
| entirety of x := 1, y := 2, z := 3 at once, the right
| hand sides in the old frame and the left hand sides in
| the new frame.
|
| So let introduces a new frame BUT only after all three
| substitutions are done.
|
| For example in order to rotate x y z (from the
| surrounding frame) to the right: (let
| ((y x) (z y) (x z)) ...)
|
| means: ((lambda (y z x) ...)
| x y z)
|
| For example: (let ((x 1) (y 2) (z 3))
| (let ((y x) (z y) (x z)) (list x y z)))
| => (3 1 2)
|
| The starred let is the weird form. It's shorthand for
| having another let in the tail each time: With A, B, C
| each standing for a form: (let* (A B C)
| ...) is defined to be (let (A) (let* (B C) ...). And so
| on. (let* (C) ...) is (let (C) ...), the base case.
| (let* (A B C) ...) is (let (A) (let (B) (let (C) ...)))).
| Does that look natural to you?
|
| (let (_) 5) is valid too.
|
| >If a programmer ever falls in the habit of sometimes
| using unstarred LET, then it's likely he'll make a
| mistake by using it where starred LET* was the right
| thing.
|
| Never happened to me so far and I'm using Lisp for 8
| years now.
|
| >It seems reasonable that when I write code, I shouldn't
| have to stop and worry about which of the gazillion
| different LET forms is appropriate
|
| You do you, but you seem to think that this is an
| aesthetic choice rather than what the mathematics
| automatically gives. Lisp was not really designed, it was
| discovered. As long as you don't break any of the
| mathematical properties, go ahead and make it like you
| want it to be.
|
| One thing you could safely do is remove the automatic
| tuple destructuring on let, basically removing the
| ability to have parallel-track bindings. Then you'd end
| up with something like Haskell: main =
| let x = 1 y = 2 z =
| 3 in let y = x
| z = y x = z
| in x => <<loop>>
| ogogmad wrote:
| 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.
| dannymi wrote:
| > It[Python] certainly does suggest there's compatibility
| with VAR syntax.
|
| Maybe. Maybe not.
|
| >It[Haskell, or do notation] also allows you to make
| declarations anywhere inside a block.
| main = do let x = 1 let y = 2
| let z = 3 let y = x z = y
| x = z print x => compilation
| error[1]
|
| Do you find that normal?
|
| How would you write what I wrote in Lisp in Haskell (for
| example using what you said)? Is it gonna need new
| concepts?
|
| Compare: main = do let x =
| 1 let y = 2 let z = 3
| let y = x let z = y let x = z
| print [x, y, z] => [1, 1, 1] (as expected--
| but it's still dumb)
|
| [1] a.hs:9:5: error: *
| Ambiguous type variable 'a0' arising from a use of
| 'print' prevents the constraint '(Show a0)'
| from being solved. Probable fix: use a type
| annotation to specify what 'a0' should be.
| These potential instances exist: instance
| Show Ordering -- Defined in 'GHC.Show'
| instance Show a => Show (Maybe a) -- Defined in
| 'GHC.Show' instance Show Integer --
| Defined in 'GHC.Show' instance Show () --
| Defined in 'GHC.Show' instance (Show a,
| Show b) => Show (a, b) -- Defined in 'GHC.Show'
| instance (Show a, Show b, Show c) => Show (a, b, c)
| -- Defined in 'GHC.Show' instance (Show
| a, Show b, Show c, Show d) => Show (a, b, c, d)
| -- Defined in 'GHC.Show' instance (Show
| a, Show b, Show c, Show d, Show e) =>
| Show (a, b, c, d, e) -- Defined in
| 'GHC.Show' instance (Show a, Show b, Show
| c, Show d, Show e, Show f) =>
| Show (a, b, c, d, e, f) -- Defined in
| 'GHC.Show' instance (Show a, Show b, Show
| c, Show d, Show e, Show f, Show
| g) => Show (a, b, c, d, e, f, g)
| -- Defined in 'GHC.Show' instance (Show
| a, Show b, Show c, Show d, Show e, Show f, Show g,
| Show h) => Show (a, b, c, d, e,
| f, g, h) -- Defined in 'GHC.Show'
| instance (Show a, Show b, Show c, Show d, Show e, Show f,
| Show g, Show h, Show i) =>
| Show (a, b, c, d, e, f, g, h, i) --
| Defined in 'GHC.Show' instance (Show a,
| Show b, Show c, Show d, Show e, Show f, Show g,
| Show h, Show i, Show j) => Show
| (a, b, c, d, e, f, g, h, i, j) --
| Defined in 'GHC.Show' instance (Show a,
| Show b, Show c, Show d, Show e, Show f, Show g,
| Show h, Show i, Show j, Show k) =>
| Show (a, b, c, d, e, f, g, h, i, j, k)
| -- Defined in 'GHC.Show' instance (Show
| a, Show b, Show c, Show d, Show e, Show f, Show g,
| Show h, Show i, Show j, Show k, Show l) =>
| Show (a, b, c, d, e, f, g, h, i, j, k, l)
| -- Defined in 'GHC.Show' instance (Show
| a, Show b, Show c, Show d, Show e, Show f, Show g,
| Show h, Show i, Show j, Show k, Show l, Show m) =>
| Show (a, b, c, d, e, f, g, h, i, j, k, l, m)
| -- Defined in 'GHC.Show' instance (Show
| a, Show b, Show c, Show d, Show e, Show f, Show g,
| Show h, Show i, Show j, Show k, Show l, Show m, Show n)
| => Show (a, b, c, d, e, f, g, h,
| i, j, k, l, m, n) -- Defined in
| 'GHC.Show' instance (Show a, Show b, Show
| c, Show d, Show e, Show f, Show g,
| Show h, Show i, Show j, Show k, Show l, Show m, Show n,
| Show o) => Show (a, b, c, d, e,
| f, g, h, i, j, k, l, m, n, o) --
| Defined in 'GHC.Show' instance Show a =>
| Show (Solo a) -- Defined in 'GHC.Show'
| instance Show Bool -- Defined in 'GHC.Show'
| instance Show Char -- Defined in 'GHC.Show'
| instance Show Double -- Defined in 'GHC.Float'
| instance Show Float -- Defined in 'GHC.Float'
| instance Show Int -- Defined in 'GHC.Show'
| instance Show Word -- Defined in 'GHC.Show'
| instance Show a => Show [a] -- Defined in 'GHC.Show'
| ...plus 13 instances involving out-of-scope types
| instance Show GHC.Stack.Types.CallStack -- Defined in
| 'GHC.Show' instance Show
| GHC.Types.KindRep -- Defined in 'GHC.Show'
| instance Show GHC.Types.Levity -- Defined in 'GHC.Show'
| instance Show GHC.Types.Module -- Defined in 'GHC.Show'
| instance Show GHC.Num.Natural.Natural -- Defined in
| 'GHC.Show' instance Show a => Show
| (GHC.Base.NonEmpty a) -- Defined in
| 'GHC.Show' instance Show
| GHC.Types.RuntimeRep -- Defined in 'GHC.Show'
| instance Show GHC.Stack.Types.SrcLoc -- Defined in
| 'GHC.Show' instance Show
| GHC.Types.TrName -- Defined in 'GHC.Show'
| instance Show GHC.Types.TyCon -- Defined in 'GHC.Show'
| instance Show GHC.Types.TypeLitSort -- Defined in
| 'GHC.Show' instance Show
| GHC.Types.VecCount -- Defined in 'GHC.Show'
| instance Show GHC.Types.VecElem -- Defined in 'GHC.Show'
| * In a stmt of a 'do' block: print x In the
| expression: do let x = 1
| let y = 2 let z = 3
| let y = x z = y
| .... .... In an equation
| for 'main': main =
| do let x = ... let y = ...
| let z = ... .... |
| 9 | print x | ^^^^^
|
| ^ Are you sure you want this in your language? :)
| tome wrote:
| > 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']
| martinflack wrote:
| You might like Guile, a form of Scheme. It lets you have
| define calls inside a begin call (progn equivalent)
| exactly as you ask.
| kragen wrote:
| 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
| kragen wrote:
| also basic variables are global. most of these languages
| let you create global variables anywhere, or at least
| anywhere outside of a subroutine
| gknoy wrote:
| 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?
| kragen wrote:
| 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
| michael1999 wrote:
| Thank you. I hadn't drawn the parallel before. But that
| matches my experience with both.
| avgcorrection wrote:
| > In C-derived languages, you can stick variable
| definitions anywhere.
|
| ANSI C89 requires that variable declarations only occur at
| the beginning of the scope.
| kazinator wrote:
| 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 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
| _let_ s.
| db48x wrote:
| Lisp existed before any of the conventions of languages
| like C were invented.
| mtlmtlmtlmtl wrote:
| 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))
| behnamoh wrote:
| 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.
| kragen wrote:
| 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
| ryan-duve wrote:
| 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.
| fiddlerwoaroof wrote:
| 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.
| dhbradshaw wrote:
| To get a nice combination of easy edits and evaluation in a
| format familiar to a pythonista, could play with Lisp in
| Jupyter:
|
| https://github.com/yitzchak/common-lisp-jupyter
| gumby wrote:
| 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.
| jrvarela56 wrote:
| 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.
| whartung wrote:
| 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.
| mtreis86 wrote:
| >There's a program that essentially gives readline behavior
| to anything that reads stdin, a readline interface.
|
| rlwrap https://man.archlinux.org/man/rlwrap.1
| lispm wrote:
| 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.
| mark_l_watson wrote:
| 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.
| andsoitis wrote:
| > Emacs with SBCL or LispWorks fits my needs better.
|
| Do you use both, rather than one predominantly?
| mark_l_watson wrote:
| 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.
| pamoroso wrote:
| 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.
| wglb wrote:
| I agree. I took a very brief look at Medley and SBCL+emacs is
| working well for me.
| commandlinefan wrote:
| 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.
| bombcar wrote:
| 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.
| Qem wrote:
| > 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.
| bombcar wrote:
| 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.
| asciimov wrote:
| 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.
| commandlinefan wrote:
| 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.
| tzs wrote:
| 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.
| commandlinefan wrote:
| > 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.
| munificent wrote:
| 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.
| myth_drannon wrote:
| And I thought I was the only one with this hobby of buying and
| reading old computer programming books.
| commandlinefan wrote:
| I must admit, though, that I buy _way_ more of them than I
| read.
| wglb wrote:
| 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".
| adamc wrote:
| The Little Schemer is one of my favorite books. Really
| delightful.
| bsder wrote:
| "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.
| pcblues wrote:
| 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-[...
| busfahrer wrote:
| 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/
| asa400 wrote:
| 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.
| anthk wrote:
| 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.
| giancarlostoro wrote:
| 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...
| jes wrote:
| 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.
| pamoroso wrote:
| Tatar's book doesn't cover or mention CLOS.
| jes wrote:
| Yeah, I was thinking of a different book. Here's the link to
| the other one:
|
| https://www.amazon.com/Object-Oriented-Programming-COMMON-
| LI...
| anthk wrote:
| Read "Common Lisp: A Gentle Introduction to Symbolic Computation"
| , much better. And funnier. Later, "Paradigms of Artificial
| Intelligence Programming".
| cyrialize wrote:
| 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.
___________________________________________________________________
(page generated 2024-02-22 23:01 UTC)