Live data from Hacker News

The empty list

tfeb.org

61–70 of 109 posts

Re: The empty list

#61

Earlier quoted context omitted.

I agree. I use quite a few languages that don't have the "scorpion in a jar" problem where an argument might be an empty list or null (some that address it with static typing, some where the two values are the same value).

I'm not familiar with the "scorpion in a jar" idiom; guessing it's something like [to kill the scorpion you have to open the jar]... care to elucidate?

It's a reference to the "Codeless Code" link I shared upthread: 'All nothings are not equal.' A language that supports null and empty list as separate things opens the risk "what happens when the user passes null to something expecting a list?"

It is, at least, something a language with good static typing can mitigate by disallowing null as a list argument. And then there's Java...

Re: The empty list

#62
post #59
post #37

Earlier quoted context omitted.

The same way you do now: (NIL) would not answer true to NULL. And it would not be EQ to NIL.

If NIL were a CONS whose CAR is NIL and whose CDR is NIL, wouldn't NIL be the same as (CONS NIL NIL) = (NIL . NIL) = '(NIL)?

It depends on what you mean by "the same". It is already "the same" in the sense that the CAR and CDR of both NIL and (NIL) are all NIL. They just aren't EQUAL, despite the fact that their CARs and CDRs are EQUAL (in fact, they are all EQ).

Re: The empty list

#63
post #59
post #37

Earlier quoted context omitted.

The same way you do now: (NIL) would not answer true to NULL. And it would not be EQ to NIL.

If NIL were a CONS whose CAR is NIL and whose CDR is NIL, wouldn't NIL be the same as (CONS NIL NIL) = (NIL . NIL) = '(NIL)?

Technically not, because a cons of two objects is not eq to another cons of the same two objects.

  (eq (cons 1 2)
      (cons 1 2))
  ;; => nil
It's memory locations, after all.

Re: The empty list

#64
post #60
post #39

Earlier quoted context omitted.

> You are conflating implementation tricks with language semantics. No, I'm not. > In Lisp, NIL is always an atom, never a cons. That depends on what you mean by "atom". If by "atom" you mean something that answers true to the ATOM predicate then yes, NIL is an atom. But if by "atom" you mean something that produces an error if you try to call CAR or CDR on it then no, NIL is not an atom, it is equal to (CONS NIL NIL…

But NIL is not equal to (CONS NIL NIL). (CONS X NIL) = '(X), a list of length 1 containing X, so (CONS NIL NIL) = '(NIL), a list of length 1 containing NIL. But NIL is a list of length 0, not a list of length 1. The wart is that it should never have been the case that you could call CAR and CDR on NIL. But even though you can wartily call them on NIL, there is still clearly a very important distinction between NIL an…

> But NIL is not equal to (CONS NIL NIL).

Yes, that's true, but that is a special case. For all other objects X and Y, if (CAR X) is equal to (CAR Y) and (CDR X) is equal to (CDR Y) then X and Y are equal. NIL and (NIL) are the only exception. And the fact that they are an exception is a consequence of the design decision to allow CAR and CDR to be called on NIL and return NIL.

> The wart is that it should never have been the case that you could call CAR and CDR on NIL.

Yes, that is the whole point.

> But even though you can wartily call them on NIL, there is still clearly a very important distinction between NIL and (CONS (CAR NIL) (CDR NIL))!

You could have as well said between NIL and (CONS NIL NIL) or just (NIL). And yes, this is true. Nonetheless, it is possible to implement NIL as a privileged cons cell with both CAR and CDR pointing to itself under the hood, and many CL implementations actually do this. It's a design decision. You have to put the warty code somewhere. You can put it in CAR and CDR, or you can put it in NULL, EQUAL, SYMBOLP, etc. But you have to put it somewhere.

Re: The empty list

#65
post #63
post #59

Earlier quoted context omitted.

If NIL were a CONS whose CAR is NIL and whose CDR is NIL, wouldn't NIL be the same as (CONS NIL NIL) = (NIL . NIL) = '(NIL)?

Technically not, because a cons of two objects is not eq to another cons of the same two objects. (eq (cons 1 2) (cons 1 2)) ;; => nil It's memory locations, after all.

I see, fair enough. I forgot to switch out of pure functional programming/referential transparency mode for this discussion. Fine, you can take NIL to be one particular (NIL . NIL) that's different from all other (NIL . NIL)s, the only particular (NIL . NIL) that is considered a list of length 0 while all other (NIL . NIL)s are lists of length 1.

Re: The empty list

#66

I like the way scheme does it, but IMO this is one of the least significant reasons scheme choices are much preferable to cl imo. It’s just a much cleaner, friendlier language overall

I really like Scheme, though the Common Lisp type system has completely won me over (especially with defstar).

Re: The empty list

#67

I've been trying to learn CL by using it to do AOC. As someone coming from mostly Python, there is a lot about it that confuses and, occasionally, frustrates me. Maybe I'm just missing something obvious but it doesn't seem like you can start with an empty list that you can repeatedly append to. The empty list is nil, and `(append nil foo)` seems to just yield `foo`, not `(list foo)`. So trying to append to that objec…

Lists in python are what are called vectors in almost every other language.

You can make a resizable vector in CL and use vector-push-extend to do things just like python.

Re: The empty list

#68
post #24

Earlier quoted context omitted.

append concatenates lists so you'd need (append nil '(foo))

It actually leaves both its arguments unaltered and creates a new list. There's another function called nconc which does concatenate them, changing its first argument to the new list (unless its first argument is NIL).

Note that append does not copy the last argument, so there is structure sharing when you use it.

Re: The empty list

#69

I've been trying to learn CL by using it to do AOC. As someone coming from mostly Python, there is a lot about it that confuses and, occasionally, frustrates me. Maybe I'm just missing something obvious but it doesn't seem like you can start with an empty list that you can repeatedly append to. The empty list is nil, and `(append nil foo)` seems to just yield `foo`, not `(list foo)`. So trying to append to that objec…

There's a huge amount of documentation telling you that NIL is an empty list.

It isn't. It has none of the mechanics of an empty list in Python etc.

It's more like a null link, and has quite a bit in common with /0 as a string terminator.

It does different things on its own and in the context of a list.

The key is that Lisp list items are stored as car (link/pointer to an item) and cdr (link to the next item/s in the list) pairs.

Lisp's dot notation makes this explicit, but it's hidden behind syntactic sugar because it's messy and hard to read.

     (a b c) is really (a . (b . (c . NIL)))
Each dot shows the cdr of the preceding car.

On its own NIL is just a constant. It has no listy features - specifically no slots for car or cdr values. Although you can do things like (length NIL) you can't change it. It's simply not a list.

You can add car/cdr pointers to it with cons. Now you can use NIL in a list.

You can use NIL as a cdr list terminator. (something . NIL) means the list is over. There are no more cdrs after it.

You can use NIL as an empty car placeholder. (NIL . something) means the car has no value, but the list continues onwards with a cdr link.

In your example (append NIL foo) actually returns (foo . NIL). NIL is being used as a cdr terminator. Lisp hides the ". NIL" because syntactic sugar.

If you (append NIL foo) again, you still get (foo) because NIL already terminates the list and there's no reason to add another NIL after it.

(push foo NIL) throws an error because push is destructive and is attempting to change NIL. Which isn't allowed.

(cons NIL a) gives you (NIL a) because NIL is being prepended as an empty car pointer. Which is how you get NILs into a list without it collapsing around itself. It's just like any other list item, except it has a null value. The list can continue past it into further cdrs in the usual way.

Anyway. This is confusing because you have a single symbol doing three different things in three different contexts. (Not even counting its use as a Boolean...) Worse, syntactic sugar and the various function internals hide this from you. And the documentation tells you something that isn't true.

So unless you look at the source and/or learn how the various functions understand and use NIL you will be confused.

The up side is a REPL is interactive and easy to play with, so it's not a huge effort to experiment and see what falls out.

Post reply on HN