NIL
lispworks.com
lispworks.com
- GETHASH returns two values: the element found (or nil), and whether it found the element or not.
- GETF (which extracts a value from a property list) has a third optional argument for the value to return of the property isn’t found. It’s easy to make a unique sentinel value if you’re worried about collisions, for instance with an uninterned symbol:
(let* ((not-found '#:not-found)
(x (getf props 'key not-found)))
(unless (eq x not-found)
;; do something only if KEY was found
))
In this case, it’s not possible for PROPS to ever contain the NOT-FOUND value.NIL is one of the things Lisp had to compromise on to truly make it a “Common” Lisp.
Now there is a real issues with nil being a symbol, Boolean false and empty list, and it is this:
In a heterogeneous, recursively varying data structure, there are are possible ambiguities as to what nil represents:
- is it a recursive element of the nested list structure: an empty sub-list?
- is it just a symbolic element, so that we consider (a b nil t) to be a list of four symbols?
- does it indicate false?
In many cases, this problem does not exist for various possible reasons:
- the data structure does not use multiple representations. Thus if nil is seen, it is known that it is a symbol, Boolean false or empty lists.
- the data structure does use nil in multiple roles, but they are disambiguated by syntax: such as fixed positions in syntactic nodes, or other conventions.
However, even if the problem is nonexistent in a given scenario, the data structure itself doesn't make it clear; there is still a possible readability problem for someone not familiar with the conventions. Someone staring at the data structure still has to guess about what is the meaning of a given nil, and refer to the documentation or, failing that, code.
Furthermore, the system itself has a "printability problem". The Lisp printer has to decide whether to render nil as nil or as (). Sometimes you see suboptimal choices like the system emiting (LAMBDA NIL (FOO)) where you'd like to see (LAMBDA () (FOO)).
This is the price we pay for the conveniences brought about by nil.
I still haven't decided if it's a good idea or not, and I've been hacking Common Lisp for more than 20 years.
They're just minor culture clashes that add up...
So this would be a good example the culture clashes I mentioned, handled badly.
We see here also the distinction between sum types and union types: Maybe Maybe a has three variants: None, Some None, and Some Some a; but (T | null) | null collapses to just T | null.
None -> '()
Some None -> '(nil)
Some Some a -> '(a)The nil / null value is an attempt to cheaply produce from type T a type like Maybe T, or Optional<T>, or basically T | nil. It's sort of practical. OTOH making false and nil indistinguishable is much less convenient than one might think, and leads to subtle errors.
I once wrote up a little rant about future software being ancient, with no changes for centuries. What might the far future be like? I was imagining a world where security and robustness were considered much more valuable than a never ending flow of new features. Could popular programs developed over many centuries evolve to a point where it made no sense to modify them?
[1] https://lisp-journey.gitlab.io/blog/state-of-the-common-lisp...
I understand that the sort of spaghetti-code from constant nil checking would be painful, but I can also imagine a lot of hidden bugs when it comes to dealing with things like user-provided data. Would love to hear the experience from Lispers who also have experience with "null"-y issues in other languages.
;; Nil and empty list are different
(nil? '())
;> false
;; Empty list is not falsy
(if '() true false)
;> true
;; Only nil is falsy
(if nil true false)
;> false
;; Prints as empty list
(pr '())
;> ()Generally when "nil" is an issue in Clojure, it's actually from the Java side, where Java doesn't "do the right thing" for "nil" and just throws a "null pointer exception" instead.
In Clojure it's mostly a problem when it hides that you just aren't getting something because of a bug, as opposed to because it truly isn't there.
The most common one for me being map lookups:
(get m :fooo)
;> nil
Now everything after will "work", because everything handles "nil" input and generally does "the right thing", like treat it as false, or returns nil if given nil, or treats it as identity, etc. But you actually just messed up the key and that's why you are getting "nil", where you wanted to do: (get m :foo)
;> "foo"
I'd say this is the most annoying case for "nil" for me in Clojure.Then you are forced to deal with the fact that you might not have a value, right away.
Imagine you have this:
Optional<String> get(String k)
{
...
}
get("fooo").orElse("not found");
This is the same bug, the bug being it's normal that "fooo" is missing, you should have written "foo" which is the real place where the data is supposed to be.Or imagine for any other reason `get` returns an Optional.empty() when it shouldn't.
It's kind of the flip side of "null". Normally nulls are bad because of nullPointerException and people forgetting to handle null. Clojure in general handles null everywhere, and so you won't get nullPointerException, instead things will just work with whatever defaults makes sense for everything that comes after and is getting the null.
That makes it harder to realize that the null is there not because it missing, but because you have a bug which makes it look like something is missing when it should be there.
(get m :foo :not-found)
Problem solved. (get m :fooo :not-found)
Is still a bug that'll be hard to debug, because your code will most likely handle :not-found and believe that it is truly not-found for some reason, but it's not, you messed up the keyword.If you're really that worried about typos and don't need an open map, Clojure has a solution as well in the form of defrecord and deftype. Now the compiler will catch the problem.
It's just an issue in general whenever the happy path includes optionality as a possible result, how do you then distinguish the happy path of getting a missing value, which you expect is a possibility given the use case, versus a bug to have set the value or have been provided the value or have queried for the value?
I think the only languages that reduce the scope of this problem are those where you can define non-nullable types. In that at least when you know for a fact your use case shouldn't have "missing" things, the compiler can catch those bugs. You can also handle this in Clojure with runtime assertions, albeit catching the bug a little later in development.
But for the times when optional is a real possibility, you still have this issue.
If you move to an explicit set of valid keys, it could be that something failed to assign them a value, etc.
Edit: I guess maybe something where the language simply has no null ever could work. Like say a field not yet assigned a value would have the value :unset, and so if someone doesn't provide a value for the field, you'd still set it, but with the new value :missing. Now technically you could distinguish a bug as `:unset` versus actually missing as `:missing`. I'm not sure this would work for dynamic use cases though, where like you don't know the full range of possibilities in advance. But if you do, you could check that nothing is `:unset` and if it is, you know there is a bug, otherwise it should be `:missing` or some other value.
Edit2: Ok, now that I played a bit more with what you said, I realized thay your solution is actually better then I thought, but it depends how you model things.
(get {:foo :missing} :fooo :unset)
;> :unset
(get {:foo :missing} :foo :unset)
:> :missing
If you model optionality as an explicit `:missing` value on a set key, like assoc the key `:foo` with value `:missing` to indicate that no value was provided for it. With the 3rd argument to `get`, you can now tell the difference between a bug or an actually missing value.It does seem to require a bit of care, and I don't know if its just.moving the issue somewhere else, but it could be an approach.
First, the whole billion dollar mistake is nil POINTERS, not the value nil. Nil in lisp is a value. This is not really a problem, and in fact there's even a design pattern of the null object pattern (https://en.wikipedia.org/wiki/Null_object_pattern) to reproduce this behavior in OOP languages.
Second, in CL, there really is no such thing as nil. Nil is the empty list, they are the same thing. you could search/replace every instance of nil in your code with '() and you'd have the same code. And before you get flashbacks of Javascript and null, just remember unlike JS, there's only a single falsey value and that is the empty list, and there is no automatic type conversion that has surprising boolean semantics.
Ultimately CL manages to turn nil punning into a very nice feature that makes the spaghetti code you're talking about unnessary while also avoiding many of the 'null'-y issues of other languages due to
1) nil is a value
2) there is only one falsey value and that is nil
3) no automatic type conversion
CL certainly has its warts, but that is not one of them. Most data types other than pairs and symbols are self-evaluating. You don't quote number literals, for instance!
That the list terminator is not self-evaluating is definitely a scheme wart. (Sadly, I'm told that motions to change this behaviour in r7rs-large were rejected.)
'(1 2)
'(1)
() ; wart
(+ . (1 . (2 . (3 . ())))) #| --> 6 |#
(quote . (1 . (2 . (3 . ())))) #| --> (1 2 3) |#
(1 2 3) is a pair and we have a rule for evaluating pairs: unless the car is a symbol denoting a special form (1 is not one such), apply the car to the cdr. So we need to quote it if we want to get it as a literal.() is a special list terminator object; it is not a pair—as this dotted list syntax shows—and it has neither car nor cdr, so our previous rule for evaluation does not apply. However we have another conventional rule: objects which are not pairs and are not symbols should evaluate to themselves. This applies to strings, characters, numbers, vectors... Why not to the list terminator?
'(1 . (2 . (3 . ())))
'(1 . (2 . ()))
'(1 . ())
()
The ' applies to the ., not to the ().http://www.ai.sri.com/~delacaze/alu-site/alu/table/Lisp-Hist...
>The Macsyma group at MIT began a project during the late 1970's called the New Implementation of Lisp (NIL) for the VAX, which was headed by White. One of the stated goals of the NIL project was to fix many of the historic, but annoying, problems with Lisp while retaining significant compatibility with MacLisp. At about the same time, a research group at Stanford University and Lawrence Livermore National Laboratory headed by Richard P. Gabriel began the design of a Lisp to run on the S-1 Mark IIA supercomputer. S-1 Lisp, never completely functional, was the test bed for adapting advanced compiler techniques to Lisp implementation. Eventually the S-1 and NIL groups collaborated. For further information about the NIL project, see ``NIL---A Perspective.''
http://www.ai.sri.com/~delacaze/alu-site/alu/table/older.htm...
>``NIL---A Perspective,'' JonL White, Macsyma User's Conference, 1979.
https://en.wikipedia.org/wiki/NIL_(programming_language)
>New Implementation of LISP (NIL) is a programming language, a dialect of the language Lisp, developed at the Massachusetts Institute of Technology (MIT) during the 1970s, and intended to be the successor to the language Maclisp.[1] It is a 32-bit implementation,[2] and was in part a response to Digital Equipment Corporation's (DEC) VAX computer. The project was headed by Jon L White,[3] with a stated goal of maintaining compatibility with MacLisp while fixing many of its problems.
false is an object which can be stored in the container, and which you could be looking for. You've just migrated the problem away from empty lists.
At some point I wrote an API-validation utility almost entirely to help manage this problem.
I think when they manually fixed it in individual cases, they did something like `(or the-list json:empty-array)`. We just had to encounter that case in practice and then make a Jira ticket for them to go in and patch over it, and then do that over again each and every time it came up :P
The utility I wrote[1] would match JSON responses against expected schemas and report the bad `null` (or otherwise), its location in the JSON, and which service it came from, in the JS console. It didn't stop errors from reaching production but it saved a lot of time debugging.
The same is true of Objective-C's object semantics, which means that language has two flavors of null pointer, one of which will scream SIGSEGV if you do anything significant with it, the other having more subtle failure modes...
Object>>isNil ^false
UndefinedObject>>isNil ^true
It drops into the debugger if you pass it a message it doesn't understand.
NIL in PicoLisp.