Most active commenters

    ←back to thread

    Hofstadter on Lisp (1983)

    (gist.github.com)
    372 points Eric_WVGG | 14 comments | | HN request time: 0.206s | source | bottom
    Show context
    susam ◴[] No.41861244[source]
    > 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.

    > cdr [x] is also defined when x is not atomic.

    However, both Common Lisp and Emacs Lisp define (car nil) and (cdr nil) to be nil. Quoting from <https://www.lispworks.com/documentation/HyperSpec/Body/f_car...>:

    > If x is a cons, car returns the car of that cons. If x is nil, car returns nil.

    > If x is a cons, cdr returns the cdr of that cons. If x is nil, cdr returns nil.

    Also, quoting from <https://www.gnu.org/software/emacs/manual/html_node/elisp/Li...>:

    > 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.

    replies(6): >>41861327 #>>41861751 #>>41862379 #>>41862873 #>>41862933 #>>41868929 #
    1. sph ◴[] No.41861751[source]
    Sadly this is not the case with Scheme and it makes for very unergonomic code, especially for a newbie like me.

    Which is a shame, because I prefer (Guile) Scheme to Common Lisp.

    replies(2): >>41862075 #>>41863892 #
    2. pfdietz ◴[] No.41862075[source]
    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.
    replies(1): >>41862909 #
    3. sph ◴[] No.41862909[source]
    They are not essential, but they make code that operates in lisp more compact and pleasant to write.

    In Scheme my code is littered with

      (if (null? lst)
          ;; handle empty case here
          ...)
    
    Simply because otherwise car throws an error. This whole section is often unnecessary in CL.
    replies(2): >>41863303 #>>41867459 #
    4. tmtvl ◴[] No.41863303{3}[source]
    But you need to handle the empty case anyway otherwise you process nils ad infinitum.
    replies(1): >>41865061 #
    5. BoiledCabbage ◴[] No.41863892[source]
    > 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?

    replies(2): >>41864352 #>>41893182 #
    6. susam ◴[] No.41864352[source]
    > 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?

    How about this?

      CL-USER> (null nil)
      T
      CL-USER> (null '(nil))
      NIL
      CL-USER>
    replies(1): >>41865006 #
    7. BoiledCabbage ◴[] No.41865006{3}[source]
    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?

    replies(2): >>41870434 #>>41893234 #
    8. erik_seaberg ◴[] No.41865061{4}[source]
    You can say

      (if lst
        ...)
    
    if the empty list is falsy, but Scheme eventually chose to add #t and #f. Oddly #f is the only false value but #t is not the only true value.
    replies(1): >>41867448 #
    9. tmtvl ◴[] No.41867448{5}[source]
    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).
    replies(1): >>41870450 #
    10. guenthert ◴[] No.41867459{3}[source]
    Now, which is more pleasant to read (arguably the more important question for all, but the most primitive of applications)?
    11. g19205 ◴[] No.41870434{4}[source]
    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.

    12. waterhouse ◴[] No.41870450{6}[source]
    An oldie: https://ashwinram.org/1986/01/28/a-short-ballad-dedicated-to...

    Describes the evolution from:

      (cdr (assq key a-list))
    
    to:

      (let ((val (assq key a-list)))
         (cond ((not (null? val)) (cdr val))
               (else nil)))
    13. kazinator ◴[] No.41893182[source]
    > 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.

    14. kazinator ◴[] No.41893234{4}[source]
    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.