Congratulations on successfully crafting a computer game! I played the game briefly this morning and I found it to be quite fun. Glad to see a 9 year old experiencing the joy of computing!
Playing computer games (and then later programming with Logo and BASIC) is how I got introduced to computers. I too wanted to develop my own computer game when I was 8 year old. But unfortunately, I neither had access to enough time with computers nor did I have sufficient programming skills back then.
I did end up fulfilling my childhood dream of developing an invaders-like game 30 years later as an adult. I too chose to implement it using plain HTML and JavaScript. I have written more about it here: <https://github.com/susam/invaders#why>.
It is very heartwarming to see that we live in an era where computers are pervasive and a 9 year old can learn all this stuff from the World Wide Web and large language models, and then implement a fully functional computer game!
I've been on the Internet since 1999, and I feel a strong sense of nostalgia for those early years. For me, the period from 1999 to 2010 was the "golden age" of the Internet. It was a time of exploration, creativity, and genuine connection. I imagine that people who joined even earlier might feel a similar nostalgia for their own era on the web.
I also wrote about my experiences and why I consider this time the golden age in a blog post here: <https://susam.net/web-golden.html>.
InputCharacter:
UnicodeInputCharacter but not CR or LF
UnicodeInputCharacter is defined as the following in section 3.3:
UnicodeInputCharacter:
UnicodeEscape
RawInputCharacter
UnicodeEscape:
\ UnicodeMarker HexDigit HexDigit HexDigit HexDigit
UnicodeMarker:
u {u}
HexDigit:
(one of)
0 1 2 3 4 5 6 7 8 9 a b c d e f A B C D E F
RawInputCharacter:
any Unicode character
As a result the lexical analyser honours Unicode escape sequences absolutely anywhere in the program text. For example, this is a valid Java program:
public class Bar {
public static void \u006d\u0061\u0069\u006e(String[] args) {
System.out.println("hello, world");
}
}
Here is the output:
$ javac Bar.java && java Bar
hello, world
However, this is an incorrect Java program:
public class Baz {
// This comment contains \u6d.
public static void main(String[] args) {
System.out.println("hello, world");
}
}
$ sml < hello.sml
Standard ML of New Jersey (64-bit) v110.99.5 [built: Thu Mar 14 17:56:03 2024]
- = hello, world
$ mlton hello.sml && ./hello
hello, world
Given how C was considered one of the "expressive" languages when it arrived, it's curious that nested comments were never part of the language.
There are 3 things I find funny about that comment: ML didn’t have single-line comments, so same level of surprising limitation. I’ve never heard someone refer to C as “expressive”, but maybe it was in 1972 when compared to assembly. And what bearing does the comment syntax have on the expressiveness of a language? I would argue absolutely none at all, by definition. :P
> ML didn’t have single-line comments, so same level of surprising limitation.
It is not quite clear to me why the lack of single-line comments is such a surprising limitation. After all, a single-line block comment can easily serve as a substitute. However, there is no straightforward workaround for the lack of nested block comments.
> I’ve never heard someone refer to C as “expressive”, but maybe it was in 1972 when compared to assembly.
I was thinking of Fortran in this context. For instance, Fortran 77 lacked function pointers and offered a limited set of control flow structures, along with cumbersome support for recursion. I know Fortran, with its native support for multidimensional arrays, excelled in numerical and scientific computing but C quickly became the preferred language for general purpose computing.
While very few today would consider C a pinnacle of expressiveness, when I was learning C, the landscape of mainstream programming languages was much more restricted. In fact, the preface to the first edition of K&R notes the following:
"In our experience, C has proven to be a pleasant, expressive and versatile language for a wide
variety of programs."
C, Pascal, etc. stood out as some of the few mainstream programming languages that offered a reasonable level of expressiveness. Of course, Lisp was exceptionally expressive in its own right, but it wasn't always the best fit for certain applications or environments.
> And what bearing does the comment syntax have on the expressiveness of a language?
Nothing at all. I agree. The expressiveness of C comes from its grammar, which the language parser handles. Support for nested comments, in the context of C, is a concern for the lexer, so indeed one does not directly influence the other. However, it is still curious that a language with such a sophisticated grammar and parser could not allocate a bit of its complexity budget to support nested comments in its lexer. This is a trivial matter, I know, but I still couldn't help but wonder about it.
But we did have dummy procedures, which covered one of the important use cases directly, and which could be abused to fake function/subroutine pointers stored in data.
Fair enough. From my perspective, lack of single line comments is a little surprising because most other languages had it at the time (1973, when ML was introduced). Lack of nested comments doesn’t seem surprising, because it isn’t an important feature for a language, and because most other languages did not have it at the time (1972, when C was introduced).
I can imagine both pro and con arguments for supporting nested comments, but regardless of what I think, C certainly could have added support for nested comments at any time, and hasn’t, which suggests that there isn’t sufficient need for it. That might be the entire explanation: not even worth a little complexity.
However, if a later C standard were to introduce nested comments, it would break the above program because then the following part of the program would be recognised as a comment:
/* /* Comment */
printf("hello */
The above text would be ignored. Then the compiler would encounter the following:
world");
This would lead to errors like undeclared identifier 'world', missing terminating " character, etc.
Given the neighboring thread where I just learned that the lexer runs before the preprocessor, I’m not sure that would be the outcome. There’s no reason to assume the comment terminator wouldn’t be ignored in strings. And even today, you can safely write printf(“hello // world\n”); without risking a compile error, right?
> Given the neighboring thread where I just learned that the lexer runs before the preprocessor, I’m not sure that would be the outcome.
That is precisely why nested comments would end up breaking the C89 code example I provided above. I elaborate this further below.
> There’s no reason to assume the comment terminator wouldn’t be ignored in strings.
There is no notion of "comment terminator in strings" in C. At any point of time, the lexer is reading either a string or a comment but never one within the other. For example, in C89, C99, etc., this is an invalid C program too:
In this case, we wouldn't say that the lexer is "honoring the comment terminator in a string" because, at the point the comment terminator '*/' is read, there is no active string. There is only a comment that looks like this:
/* Comment
printf("hello */
The double quotation mark within the comment is immaterial. It is simply part of the comment. Once the lexer has read the opening '/*', it looks for the terminating '*/'. This behaviour would hold even if future C standards were to allow nested comments, which is why nested comments would break the C89 example I mentioned in my earlier HN comment.
> And even today, you can safely write printf("hello // world\n"); without risking a compile error, right?
Right. But it is not clear what this has got to do with my concern that nested comments would break valid C89 programs. In this printf() example, we only have an ordinary string, so obviously this compiles fine. Once the lexer has read the opening quotation mark as the beginning of a string, it looks for an unescaped terminating quotation mark. So clearly, everything until the unescaped terminating quotation mark is a string!
Oh wow, I didn’t remember that, and I did start writing C before 99. I stand corrected. I guess that is a little surprising. ;)
Is true that many languages had single line comments? Maybe I’m forgetting more, but I remember everything else having single line comments… asm, basic, shell. I used Pascal in the 80s and apparently forgot it didn’t have line comments either?
Some C compilers supported it as an unofficial extension well before C99, so that could be why you didn't realise or don't remember. I think that included both Visual Studio (which was really a C++ compiler that could turn off the C++ bits) and GCC with GNU extensions enabled.
That's my recollection, that most languages had single line comments. Some had multi-line comments but C++ is the first I remember having syntaxes for both. That said, I'm not terribly familiar with pre-80s stuff.
I was barely too young for this to make much of an impact at the time, (but older than many, perhaps most, here), I understand why C was considered a "high level language", but it still hits me weird, given today's context.
Lexing nested comments requires maintaining a stack (or at least a nesting-level counter). That wasn’t traditionally seen as being within the realm of lexical analysis, which would only use a finite-state automaton, like regular expressions.
Except that text inside #if 0 still has to lex correctly.
(unifdef has some evil code to support using C-style preprocessor directives with non-C source, which mostly boils down to ignoring comments. I don’t recommend it!)
> Except that text inside #if 0 still has to lex correctly.
Are you sure? I just tried on godbolt and that’s not true with gcc 14.2. I’ve definitely put syntax errors intentionally into #if 0 blocks and had it compile. Are you thinking of some older version or something? I thought the pre-processor ran before the lexer since always…
This article has been a fascinating read, by the way. Kudos to the maintainer of the Gist post. I am also sharing these corrections as comments on the Gist post.
EDIT #1: Downloaded a copy of the original Scientific American article from https://www.jstor.org/stable/24968822 and confirmed that indeed the functions "oval" and "snot" are misspellings of "eval" and "snoc".
EDIT #2: Fixed typo in this comment highlighted by @fuzztester below.
> Attempting to take the car or cdr of nil causes (or should cause) the Lisp genie to cough out an error message, just as attempting to divide by zero should evoke an error message.
Interestingly, this is no longer the case. Modern Lisps now evaluate (car nil) and (cdr nil) to nil. In the original Lisp defined by John McCarthy, indeed CAR and CDR were undefined for NIL. Quoting from <https://dl.acm.org/doi/pdf/10.1145/367177.367199>:
> Here NIL is an atomic symbol used to terminate lists.
> car [x] is defined if and only if x is not atomic.
> Function: car cons-cell ... As a special case, if cons-cell is nil, this function returns nil. Therefore, any list is a valid argument. An error is signaled if the argument is not a cons cell or nil.
> Function: cdr cons-cell ... As a special case, if cons-cell is nil, this function returns nil; therefore, any list is a valid argument. An error is signaled if the argument is not a cons cell or nil.
I was curious what it is like on Maclisp. Here is a complete telnet session with Lars Brinkhoff's public ITS:
$ telnet its.pdp10.se 10003
Trying 88.99.191.74...
Connected to pdp10.se.
Escape character is '^]'.
Connected to the KA-10 simulator MTY device, line 0
^Z
TT ITS.1652. DDT.1548.
TTY 21
3. Lusers, Fair Share = 99%
Welcome to ITS!
For brief information, type ?
For a list of colon commands, type :? and press Enter.
For the full info system, type :INFO and Enter.
Happy hacking!
:LOGIN SUSAM
TT: SUSAM; SUSAM MAIL - NON-EXISTENT DIRECTORY
:LISP
LISP 2156
Alloc? n
*
(status lispversion)
/2156
(car nil)
NIL
(cdr nil)
NIL
^Z
50107) XCT 11 :LOGOUT
TT ITS 1652 Console 21 Free. 19:55:07
^]
telnet> ^D Connection closed.
$
I recall reading that in early versions of Maclisp, taking the CAR or CDR of NIL worked differently: Taking its CAR would signal an error as you would expect, however taking its CDR would return the symbol plist of NIL, as internally the operation of CDR on the location of a symbol would access its plist, and that's how it was commonly done before there was a specific form for it (and it actually still worked that way into Lisp Machine Lisp, provided you took the CDR of the locative of a symbol).
Apparently the behaviour of the CAR and CDR of NIL being NIL was from Interlisp, and it wasn't until the designers of Maclisp and Interlisp met to exchange ideas that they decided to adopt that behaviour (it was also ostensibly one of the very few things they actually ended up agreeing on). The reason they chose it was because they figured operations like CADR and such would be more correct if they simply returned NIL if that part of the list didn't exist rather than returning an error, otherwise you had to check each cons of the list every time. (If somebody can find the source for this, please link it!)
What's saved is that your code just calls that function, rather than open-coding that check.
Suppose you wrote this code in more than two or three places:
(if (and (consp x) (consp (cdr x))
(car (cdr x)))
you might define a function for that. Since there is cadr, you don't have to.
Also, that function may be more efficient, especially if our compiler doesn't have good CSE. Even if x is just a local variable, there is the issue that (cdr x) is called twice. A clever compiler will recognize that the value of x has not changed, and generate only one access to the cdr.
The function can be coded to do that even in the absence of such a compiler.
(That is realistic; in the early lifecycle of a language, the quality of library functions can easily outpace the quality of compiler code generation, because the library writers use efficient coding tricks, and perhaps even drop into a lower level language where beneficial.)
The function call gives us all that for free: (cadr (complex-expr y)). The argument expression is evaluated once, and bound to the formal parameter that the function refers to, and the function body can do manual CSE not to access the cdr twice.
It would be considered "the right thing" to do something that's so common you probably want it without asking. I don't think CADR would check for NIL since it's meant to be equivalent to (car (cdr x)), so if you wanted a safe list operation you would have to check it like this: (I'll use CADADR because it makes the issue more apparent)
(and (car x)
(cadr x)
(cadar x)
(cadadr x))
You would have to write this every time you want to see if there's a really CADADR, whereas if CAR and CDR can return NIL then you can just write (cadadr x) and CADADR can still be defined as (car (cdr (car (cdr x)))) and have the desired behaviour.
Any argument of the form "you have to write this idiom" is covered by "so either it doesn't happen often, or one can use a macro". There's cognitive overhead to using a macro, but there's also cognitive overhead to remembering car and cdr work on nil. The latter is already paid for, so changing Common Lisp now doesn't make sense, but in an alternate world with a different design it would be a point.
There's more 'cognitive overhead' to making CADADR etc. a macro that expands to the above when CAR and CDR don't work that way, since then its implementation isn't consistent with what it's meant to be. If you made it a macro with a different name then you have two slightly different versions of CADADR and every other accessor, which is even more overhead. Apparently this idiom happened often enough that it was deemed desirable to simply make it the default behaviour. Accommodating common idioms is a pattern of "the right thing" design, from which Lisp is heavily derived, and if you're not a Lisp programmer then keeping this philosophy in mind is a good way to not have misconceptions about the design of the system.
However, thinking in terms of 'cognitive overhead' for a very minor design choice is very silly. I don't suffer any 'cognitive overhead' from having CAR and CDR work on NIL when I write Common Lisp because I'm used to it, but I do suffer 'cognitive overhead' when they don't in Scheme, which is the 'alternate world with a different design'. I am incredulous to the idea that one is actually superior to the other, and suppose that it is simply a matter of preference.
The use of car and cdr are such a surprisingly concrete implementation detail in the birth of a language that was designed to be mathematical. The most basic and famous operators of "List Processor" were created to operate not on lists but on conses, an element in a particular machine representation that Lisp uses to build data structures! Not only are conses not always interpreted as lists, but a very very important list, the base case for recursive functions on lists, is not represented by a cons.
Sixty years later, most Lisp programs are still full of operations on conses. A more accurate name for the language would be "Cons Processor!" It's a reminder that Lisp was born in an era when a language and its implementation had to fit hand in glove. I think that makes the achievement of grounding a computer language in mathematical logic all the more remarkable.
Does Scheme even have NIL in the sense that other Lisps like CL or Elisp have? I mean in Common Lisp, we have:
CL-USER> (symbolp nil)
T
CL-USER> (atom nil)
T
CL-USER> (listp nil)
T
Similar results in Emacs Lisp. But in MIT Scheme, we get:
1 ]=> nil
;Unbound variable: nil
Of course, we can use () or (define nil ()) to illustrate your point. For example:
1 ]=> (car ())
;The object (), passed as the first argument to car, is not the correct type.
But when I said NIL earlier, I really meant the symbol NIL that evaluates to NIL and is both a LIST and ATOM. But otherwise, yes, I understand your point and agree with it.
> Does Scheme even have NIL in the sense that other Lisps like CL or Elisp have?
No. It has an empty list, which is a singleton atomic value whose type is not shared with any other object, and it has a boolean false value, which is distinct from the empty list. A user can create a symbol named NIL, but that symbol has no characteristics that distinguish it from any other symbol. You can, of course, bind NIL to either the empty list or boolean false (or any other value) but it can only have one value at a time (per thread).
You need to read more carefully. The claim was not that there is no NIL in Scheme, the claim was that Scheme does not have a "NIL in the sense that other Lisps like CL or Elisp have". There is a NIL is Scheme, but it's just a symbol like any other with no privileged status. Also, in colloquial use, the word "nil" is often taken to be a synonym for "the empty list" even when talking about Scheme.
I don't believe so, standardly. Guile scheme added the value `#nil' which is equivalent to NIL and distinct from #f and the empty list, but this was done in order to support Emacs Lisp.
I'm not a LISPer but this just seems more correct to me, since stricter is usually more correct.
Ruby (not a lisp but bear with me) started to do this more correctly IMHO where a nil would start throwing errors if you tried to do things with it BUT it would still be equivalent to false in boolean checks.
It depends on what you are trying to optimize for. There is a benefit to punning the empty list and boolean false. It lets you shorten (not (null x)) to just x, and that is a common enough idiom that it actually makes a difference in real code. And there is a benefit to being able to say or type "nil" instead of "the empty list" because "nil" is shorter. But yeah, for modern production code, I agree that stricter is better, all else being equal.
That depends on what you mean by "purely functional Lisp". You can write purely functional code in any Lisp, and you can compile any Lisp to machine language, and this has been true for decades. AFAIK there is no Lisp that enforces purely functional programming, but it's easy to build one if that's what you want.
if from a syntactic-flavor perspective, endless parentheses turn me off, but also cleanly map to significant indentation (where any new open paren is a new indentation level and a close paren maps to a backdent), has anyone tried a Lisp that uses indentation instead of parens?
I'm probably failing to consider edge cases but it seems like a potentially simple tweak that might make lisps more palatable to many
imagine that, a lisp without parens... (empty cons literals... crap, that's 1 edge case!)
> I'm probably failing to consider edge cases but it seems like a potentially simple tweak that might make lisps more palatable to many
Lisp came out in 1960. The s-expression-only syntax was an accident or a discovery - depending on one's view. Over the many years no attempt to add significant indentation syntax without parentheses gained more than a few users. Syntax variants without parentheses (and no significant indentation) only had a marginally better fate. Sometimes it even contributed to the failure of Lisp derived languages (-> Lisp 2, Dylan)...
Alternative syntaxes for Lisp dialects, some of them indentation-sensitive, have been proposed numerous times over the entire history of the Lisp family.
From the start, John MacCarthy believed that Lisp would be programmed using M-expressions and not S-expressions. M-expressions are still quite parenthetical, but have some syntactic sugar for case statements and such.
In the second incarnation of the Lisp project, which was called Lisp 2, MacCarthy's team introduced an Algol-like syntactic layer transpiling to Lisp. This was still in the middle 1960's! The project didn't go anywhere; Lisp 1.5 outlived it, and is the ancestor of most other Lisp stuff.
In the early 1970's, Vaughan Pratt (of "Pratt parser" fame) came up with CGOL: a another alternative programming language syntax layer of Lisp.
Scheme has a "sweet expressions" SRFI 110 which I think was originated by David Wheeler. It is indentation-based syntax.
The Racket language has numerous language front ends, which are indicated/requested in the source file with #lang. I think one of them is sweet expressions or something like it.
Those are just some of the notable things, not counting lesser known individual projects.
What do you think is more likely, that you are the first person to ever think of this, or that others have tried to do this and failed for some reason? The more interesting question is: what is that reason? I'm not going to tell you the answer, you will learn more if you figure it out yourself, but here's a hint: look at Python's syntax, and ask yourself if it is possible to write an editor that auto-indents Python. (Second hint: look at what happens when you edit Python code in Emacs. Third hint: look at what happens when you put in PASS statements.)
I never suggested that I was the first person to think of this; not having dealt with any Lisp since (hmmm) 1990 via Scheme in my introductory CS 212 class at Cornell probably has something to do with my ignorance of the prior art in this area. I do like your approach of breadcrumbing me instead of giving me the answer, though... best I can guess is "tooling" and simply that S-expressions are simply too embedded in the minds of the Lisp community at this (or previous) point(s).
I also don't deal with significant-indentation in languages usually (and have a strong Python distaste); though I've been playing with Roc (https://www.roc-lang.org/), which has this, and have used HAML (https://haml.info/) in the past, where it seemed useful. I suppose auto-indenting is impossible in a significant-indentation language depending on what the editor can intuit based on how the previous line ended, but I don't think I'd need that feature as long as it simply held the current indentation and just let me hit Tab or Backspace. (I could see things becoming a mess if you manage to screw up the indentation, though.)
I did research "sweet expressions" (which are apparently also called T-expressions) and found the prior art there in Scheme and Lisp, and a library called "sweet" for Racket (which is another intriguing lisp dialect!). These might have gotchas, but apparently they've sufficiently solved the problem enough to be usable.
I do simply like how "T-expressions" look. Which is something I guess I care about, although I know that's not a universal among coders. (My guess is that those who care about such things are simply not 100% left-brained about their coding and are invested in the "writing" aspect of the craft.)
I'm very tied to Common Lisp, but I'm perfectly fine with the idea of a lisp in which car and cdr would be undefined on nil. Also, I'd be fine with a lisp in which () is not a symbol. I don't think these features of Common Lisp are essential or all that valuable.
I do prefer nil being the false value as well as the empty list, even if it makes it more awkward to distinguish between 'there is a result, but the result is an empty list' and 'there are no results'. But that has nothing to do with car and cdr in Common Lisp treating nil as though it were `(cons nil nil)'. The only value in that I can see is would be if rplaca and rplacd can do some useful things with that (so `(setf (car symbol-that-currently-points-at-nil) foo)' and `(setf (cdr stcpat) bar)' do those useful things).
> Sadly this is not the case with Scheme and it makes for very unergonomic code,
How so? If car of nil returns nil, then how does a caller distinguish between a value of nil and a container/list containing nil?
The only way is they can check to see if it's a cons pair or not? So if you have to check if it's a cons pair then you're doing the same thing as in scheme right?
I may be missing something, but isn't it effectively the same amount of work just potentially? Need to check for nil and need to check if it's a pair?
> how does a caller distinguish between a value of nil and a container/list containing nil
Very easily; but the point is that it's very often easy to design things so that the caller doesn't have to care.
For instance, lookup in an associative list can just be (cdr (assoc key alist)).
If the key is not found, assoc returns nil, and so cdr returns nil.
Right, so when we use this shortcut, we have an ambiguity: does the list actually have that key, but associated with the value nil? Or does it not have the key.
Believe it or not, we can design the data representation very easily such that we don't care about the difference between these two cases; we just say we don't have nil as a value; a key with a value nil is as good as a missing key.
This situation is very often acceptable. Because, in fact, data structures are very often heavily restrained in what data types they contain. Whenever we assert that, say, a dictionary has values that are, say, strings, there we have it: values may not be nil because nil is not a string. And so the ambiguity is gone.
A nice situation occurs when keys are associated with lists of values. A key may exist, but be associated with an empty list (which is nil!). Or it may not exist. We can set things up so that we don't care about distinguishing these two. If key K doesn't exist then K is not associated with a list of items, which is practically the same as being associated with an empty list of items. If we split hairs, it isn't, but in a practical application things can be arranged so it doesn't matter.
I think that's my point. You still need a separate call to distinguish the nil rom the list of nil case.
At that point, if you're making the two calls how is LISP's behavior any more ergonomic than Scheme. I'm not saying it's not possible, I just don't see it.
Can you show code between the two and how one is much worse than the other?
We can declare that our code only works with lists of numbers, or lists of strings. Therefore, nil is not expected. If (car list) returns nil, it can only be that the list is empty, because if it were not empty, it would return a number, or string, or widget or whatever the list holds.
Even when we have a heterogeneous list in Lisp, like one that can have symbols, numbers, strings or widgets, we can almost always exclude nil as a matter of design, and thus cheerfully use the simpler code.
We cannot exclude nil when a list contains Boolean values, because nil is our false.
We also cannot exclude it when it contains lists, because nil is our empty list.
The beauty is that in many situations, we can arrange not to have to care about the distinction between "item is missing" and "item is false" and "item is an empty list", and then we can write terser code.
When you see such terse code from another programmer, you know instinctively what the deal is with how they are treating nil before even looking at any documentation or test cases.
cons is an adt and fundamental building block used to build lists (which is a builtin datatype) it's also used to build other data types. the property we're discussing is useful when you're operating on those other data types, rather than lists. when you're designing those other data types you have to be aware that null can be both the absence of value and a value, so you design those other data types appropriately. the property we're discussing becomes useful and handy when you don't care about that distinction, which is quite often in practice.
for example a useful datatype is an association list. (setq x ((a . 1) (b . 2) (c . nil)))
you can query it by calling (assoc 'a x) which is going to give you back a cons cell (a . 1) in this case. now the presence or absence of this cell indicates the association. if you want to know explicitly that C is nil, then you have an option to, and it's similar in function call counts to Scheme. if you don't care though about the distinction you can do (cdr (assoc 'a x)) which is going to give you 1. doing (cdr (assoc 'foo x)) will give you nil without erroring out. it's a pretty common pattern.
in case of established data types like association list, you will probably have a library of useful functions already defined, like you can write your own getassoc function that hides the above. you can also return multiple values from getassoc the same way as gethash does the first value being the value, and the second value being whether or not there's a corresponding cons cell.
but when you define your own adhoc cons cell based structures, you don't have the benefit of predefined functions. so let's say you have an association list of symbols to cons cells (setq x ((a . (foo . 1)) (b . (bar . 2)) (c . nil))). if I want to get foo out of that list, I'll say (cadr (assoc x 'a)) which will return foo. doing (cadr (assoc x 'c)) or (cadr (assoc x 'missing)) will both return nil. these later manipulations require extensive scaffolding in Scheme.
is there a term to describe the language design choice (reminds me of SQL, btw, where it is equally bad IMHO) where doing things to nil just returns nil without erroring? I want to call it "bleeding nils/NULLs" if there isn't another term yet.
As stated, I think this design choice is terrible, especially if nil isn't equivalent to false in boolean comparisons (as it is in Ruby and Elixir- with Elixir actually providing two types of boolean operators with slightly different but significant behavior; "and" will only take pure booleans while "&&" will equate nil with false). It might mean cleaner-written code upfront but it's going to result in massively-harder-to-debug code because the actual error (a mishandled nil result) might only create a visible problem many stack levels away in some completely different part of the code.
There really should be two different kinds of cons cells, one for "proper" linked lists and another for general purpose consing. The difference is that the cdr of the first kind of cons cell (I'll call it a PL-cons) can only be NIL or another PL-cons, not anything else. This would eliminate vast categories of bugs. It would also make the predicate for determining is something was a proper list run in constant time rather than O(n). (There would still be edge cases with circular lists, but those are much less common than non-proper lists.)
Playing computer games (and then later programming with Logo and BASIC) is how I got introduced to computers. I too wanted to develop my own computer game when I was 8 year old. But unfortunately, I neither had access to enough time with computers nor did I have sufficient programming skills back then.
I did end up fulfilling my childhood dream of developing an invaders-like game 30 years later as an adult. I too chose to implement it using plain HTML and JavaScript. I have written more about it here: <https://github.com/susam/invaders#why>.
It is very heartwarming to see that we live in an era where computers are pervasive and a 9 year old can learn all this stuff from the World Wide Web and large language models, and then implement a fully functional computer game!