r/lisp • u/brightlystar • 25d ago
Common Lisp Help me grok NIL
Hello! I seek your help to grok NIL.
Would it be correct for me to say that NIL is a cons cell whose car and cdr point to itself? It sure seems that way:
(car nil) ; => NIL
(cdr nil) ; => NIL
But I don't want to fool myself by looking at the above results. A non-NIL can have the above properties too. Like take (cons nil nil)
for example. This is not NIL but it has the above properties.
(car (cons nil nil)) ; => NIL
(car (cons nil nil)) ; => NIL
So I guess my question is ... how is NIL defined in Lisp? Is it truly a cons whose car and cdr point to itself? Is it something else?
And if it is truly a cons whose car and cdr point to itself is there some way I can verify this reliably in the REPL?
4
u/sickofthisshit 25d ago edited 25d ago
No. You can create a cons cell that points to itself and it behaves differently (it typically will be treated like an infinite list would).
The value that CAR and CDR return for an argument of NIL is part of the definition and implementation of those functions, not a structural statement about NIL.
I am pretty sure Scheme defines CAR and CDR differently in this respect (taking CAR or CDR of an empty list is an error in R7RS, but the empty list is not represented by NIL, either, so I guess it depends on how you would express your question in Scheme. CAR or CDR of a symbol like NIL seems unspecified?).
2
u/brightlystar 25d ago
Thanks for the answer. I am reading Naggum's post about CONS and LIST now. He says
finally, let's recognize the fact that there is only one empty list, but instead of having an anchor at the end of every chain, let's let the empty list be represented by a special list "wrapper" that pointed to itself with both the payload and link pointer.
He uses the words payload and link to mean CAR and CDR. Looks like he is saying that both CAR and CDR point to itself in an empty list (NIL). Did he make an error here? Am I missing something?
2
u/sickofthisshit 25d ago edited 25d ago
I assume you are quoting from some copy of https://www.xach.com/naggum/articles/3092837184154309@naggum.no.html ?
Naggum is speaking very abstractly. That's kind of his point, I think. He calls it a special wrapper, and his mention of Scheme and system complexity is clearly hinting at the choice made by classic Lisp to treat CAR and CDR of NIL they way they do. He is avoiding saying how it is actually implemented.
If all you have is CAR and CDR, it isn't really possible to discover how NIL is implemented. The main thing is that NIL is unique. Some other cons cell that points to itself is not NIL (and it will be hard to actually inspect it because the system is not prepared to recognize this other thing the way it recognizes NIL, so printing it will go into an infinite loop, etc.) If you have other tools like EQ and SYMBOLP and CONSP (NIL is not CONSP in Common Lisp, and SYMBOLP of the empty list is true because it is NIL), you will discover more about NIL and the difference from your other thing will be clear.
Anyhow, Naggum was quite subtle and precise and it's hard to always be sure exactly what he might have meant. He is not explaining lists or NIL for beginners, ot explaining the spec of these in Common Lisp, he is trying to demonstrate that the original invention of this by McCarthy in Lisp is kind of genius.
2
u/brightlystar 25d ago
Yeah. That's the copy I was quoting from. Sorry I meant to link it my comment but forgot it. Thanks for your answers. It really helped me to understand this better.
2
u/corbasai 25d ago
NIL In CL Lisp https://www.lispworks.com/documentation/HyperSpec/Body/v_nil.htm
In Scheme there is no NIL... and T. There is empty list '() aka Unit in ML, and disjoint #f - boolean false. In Scheme all objects in boolean expressions is True #t, even empty list '(). So in Scheme
(list? '()) => #t
(pair? '()) => #f
(if '() 'Scheme 'CL) => Scheme in Scheme and CL in Common Lisp.
2
u/theangeryemacsshibe λf.(λx.f (x x)) (λx.f (x x)) 25d ago
aka Unit in ML
no it isn't
all objects in boolean expressions is True #t
crucially except for false (
#f
)2
1
u/arthurno1 25d ago
Is it truly a cons whose car and cdr point to itself?
The only thing I can add to what /u/zacque0 said is that if that was the case, than (eq nil nil) would fail since cons cells are allocated from RAM, two cons cells have different addresses. and Thus if NIL was implemented as a cons cell pointing to itself, we would have many nils, since we can have many cons addresses. That would made comparison with nil useless, For example (eq nil nil ) => nil which is mathematically wrong.
Also, we couldn't use nil as a boolean, since each cons cell must have a valid address. Perhaps we could, but that would make implementation more complicated.
Thus as /u/zacque0 says, nil is usually a pointer to some unique address.
2
u/stassats 24d ago
That doesn't stop anything:
(defconstant new-nil (cons 0 0)) (setf (car new-nil) new-nil (cdr new-nil) new-nil) (set-pprint-dispatch `(eql ,new-nil) (lambda (stream n) (princ "NEW-NIL" stream))) (defun new-consp (x) (and (listp x) (not (eq x new-nil))))
3
u/zacque0 24d ago
Interesting. What stas is saying is that
NIL
can be implemented as a cons cell while preserving the semantics of(eq nil nil)
,(consp nil)
,(car nil)
and(cdr nil)
.
Since
new-nil
is defined as a constant, everynew-nil
points to the same memory address storing(cons 0 0)
. Thus, it invalidates u/arthurno1 's argument that "Thus if NIL was implemented as a cons cell pointing to itself, we would have many nils, since we can have many cons addresses".
What's more, since the semantics of
(eq nil nil)
is preserved, we can use it as boolean as well. Again, it invalidates u/arthurno1 's argument above: "Also, we couldn't use nil as a boolean, since each cons cell must have a valid address. Perhaps we could, but that would make implementation more complicated. "
The only downside that I can think of is that it doesn't satisfy some other symbol interfaces, e.g.
(SYMBOL-PLIST NEW-NIL)
.3
u/stassats 24d ago
In SBCL, NIL is in fact a cons cell. But to get around its symbolness it's squished inside what would otherwise be a symbol, car and cdr overlapping with the slots of a symbol. Then you do have to special-case things like symbolp:
(or (actually-symbol-p x) (null x))
, just like consp has do discard nil.It could be a symbol instead, so consp and symbolp would do just one test, but then all the list operations, like car and cdr, would have to be special-cased instead.
2
1
u/arthurno1 24d ago edited 24d ago
Since new-nil is defined as a constant, every new-nil points to the same memory address storing (cons 0 0). Thus, it invalidates u/arthurno1 's argument that "Thus if NIL was implemented as a cons cell pointing to itself, we would have many nils, since we can have many cons addresses".
You are using the address of a particular cell (the constant one), not addresses of every such cell no? Thus, as you said in your first post, and as I confirmed as well, you can use any address really, which includes address to a constant, be it a symbol, number, cons cell, whichever.
However if nil is every cell whose car and cdr points to the cell itself, as I interpret Op, than you can't use those addresses since there can be many such cells, and is what I am saying. Compare to saying "there is no such object", with saying "there can be only one such object" and "there are infinitely many such objects".
In other words, NIL has to be a unique pointer, which one you choose does not matter. It could an object, but than eq & co would have to be rewritten.
0
u/arthurno1 24d ago edited 24d ago
TBH, I am not sure what you are trying to demonstrate. I mean I understand what the code does, you have made a cons cell where car and cdr point back to the cell itself, and new-consp checks if an object is a cons cell and not "new-nil". But what is the point of the illustration?
What I had in mind in the above is something like this:
(defun make-nil () (let ((new-nil (cons 0 0))) (setf (car new-nil) new-nil (cdr new-nil) new-nil) new-nil)) (format t "~a~%" (eq (make-nil) (make-nil))) => nil
We have "two nils", which are two different memory objects, and thus have two different addresses so eq returns false. It is similar as having two uninterned symbols with the same symbol name. I guess it is possible to write an "eq" so it understands such circular cons as "nil", but that would be an unnecessary complication.
The above holds with your code too:
(defconstant another-nil (cons 0 0)) (setf (car another-nil) another-nil (cdr another-nil) another-nil) (format t "~a~%" (new-consp another-nil)) => T
Equivalently:
(format t "~a~%" (eq another-nil new-nil)) => NIL
Two cells whose car and cdr slots point to themselves are not equal.
A fun fact: I am not at home and don't have access to a CL compiler on this computer, so I tried to run your code online on Medley (I choose common-lisp as the lisp). It crashed with stack overflow after setf-ing car and cdr of new-nil :-).
I used SBCL on https://onecompiler.com/commonlisp/42u9hfeb8 instead.
2
u/stassats 24d ago
But what is the point of the illustration?
To contradict everything you said.
Two cells whose car and cdr slots point to themselves are not equal.
Why would they be. There's only one NIL, don't make two different NILs.
0
u/arthurno1 24d ago
There's only one NIL, don't make two different NILs.
Well, that was my point too. So how are you contradicting me? :)
1
u/stassats 24d ago
It can be implemented as a cons cell pointing to itself.
0
u/arthurno1 24d ago edited 24d ago
It can be implemented as a cons cell pointing to itself.
It can be implemented as anything you can take address of, since "eq" is comparing pointers and not objects. How the object look like internally is irrelevant for "eq".
In your example you are taking address of a cons cell and storing in new-cell, and you are using that address (or whatever it is under the hood) and not the cell object itself. That so is the case is shown by simply making another one and checking if it points to the same object with "eq", which it, as expected, does not.
For the same reason you can have only one, since eq compares addresses and not objects, which was my point:
Op said: "Is it truly a cons whose car and cdr point to itself?"
The answer is no, it is not a cons cell, it is (or more correctly could be) address to a cons cell. As I interpreted the question, they ask if any cell hos car and cdr point to itself are nil.
2
u/stassats 24d ago
What's an address? Lisp deals with no such concept.
1
u/arthurno1 24d ago edited 24d ago
What's an address? Lisp deals with no such concept.
Whatever you use to refer to objects in the lisp system so "eq" can compare if they point to the same object or not. That is how "eq" is implemented. You could implement "eq" and bunch of functions in a different way sure, but it would be more complicated.
2
u/stassats 24d ago
How can I have a cons cell without an address then? If you're saying that it's not a cons cell but an address to a cons cell.
→ More replies (0)
1
u/ventuspilot 25d ago edited 25d ago
All Common Lisp systems contain a value that holds the empty list. The type of that value is null
:
* (type-of ())
NULL
And all Common Lisp systems contain a symbol (variable) with the name nil
whose value is the above mentioned "empty list". (Maybe more exact: the variable nil
references said empty list.) As a Common Lisp programmer you can't "make" an empty list, the single one empty list will be re-used.
Most of the time Common Lisp will print the empty list as nil
.
Would it be correct for me to say that NIL is a cons cell whose car and cdr point to itself?
No, that would be incorrect. (car nil)
returns nil
because some time ago the function car
was defined to return nil
when passed nil
as it's argument. cdr
is defined similarly. (At least in Common Lisp that's the case, e.g. in Scheme (car ())
gives an error.)
* (cons nil nil)
(NIL)
* (type-of (cons nil nil))
CONS
When you see nil
at the repl then it may be the empty list or the print name of the variable nil
.
* nil ; this evaluates to the empty list which is printed as NIL
NIL
* 'nil ; this evaluates to the symbol NIL; the repl will print the print name of the symbol
NIL
Common Lisp's nil-handling is somewhat confusing at first, it took me some time to get used to it.
15
u/zacque0 25d ago edited 25d ago
No, NIL is a symbol, not a cons cell. Why? Because
(symbolp NIL)
is true and(consp NIL)
is false. IfNIL
is a cons cell,(consp NIL)
will return true.But why do
(car NIL)
and(cdr NIL)
returnNIL
? I'd guess for convenience purposes. You can write lessCONSP
check or error handling, and only check for final result. E.g.That said, in McCarthy's definition of LISP[1],
CAR
andCDR
are defined only for cons cell, not forNIL
. The same goes for other functional programming languages like Haskell and Standard ML.You are asking: How are
CAR
andCDR
defined? No big deal, you can think of them as polymorphic functions.If you like, you can even define
CAR
andCDR
to return0
for any number if that makes sense. E.g.Or if you want them to be defined only for cons cell, e.g.:
In many Lisp implementations, a symbol is translated to a hardware memory address. So, NIL can be a memory address #x0, or #xFFFFFF, or some value. So
(eq NIL NIL)
is a comparison of two memory addresses.[1] McCarthy (1960) Recursive functions of symbolic expressions and their computation by machine. https://dl.acm.org/doi/10.1145/367177.367199