Hacker News new | comments | show | ask | jobs | submit login

I've programmed a lot in various dialects of Lisp, and I like car and cdr. Using names like first and rest might make a language more attractive to new users, but there is something to be said for designing at least some languages for experienced users rather than new ones, and it doesn't take much experience (a week?) for the words car and cdr to mean the left and right halves of a cons cell to you. And once they do, they're better than first and rest, because

1. They're shorter.

2. They're the same length, which means in practice that a lot of code lines up in a way that's easier to read.

3. They look similar to one another, which telegraphs that they're part of the same set of list access tools. Code that munges lists looks like it munges lists.

4. Their meaning is precise: car just takes the first element of a list. Whereas something called first might reasonably be expected to also give you the first element of a vector, or a string.

5. When you're operating on lists as trees, the names first and rest are actively misleading. The car and cdr are the left and right subtrees, not the first and rest of something.




> Using names like first and rest might make a language more attractive to new users, but there is something to be said for designing at least some languages for experienced users rather than new ones, and it doesn't take much experience (a week?) for the words car and cdr to mean the left and right halves of a cons cell to you.

When designing Rust, we quickly learned that the idea of picking short names to satisfy experienced users at the cost of new ones doesn't work in practice. Too many users complain that the language is too hard to learn and ugly; it took years for "Rust is so ugly, it's the resurrection of Perl" to finally stop being a meme. If we had stuck to our guns with short keywords, Rust might have been dead by now.

Choosing unfamiliar syntax such as car and cdr worked for Lisp because, during the '70s, Lisp as a whole was novel enough to gain a sizable following. It doesn't work today. (And note that, even then, Lisp lost a lot of potential users due to the S-expression based syntax.) I'm firmly in the "first" and "rest" camp, because history has shown that readable languages much more frequently go on to be successful than languages with idiosyncratic syntactic choices.


A sometimes overlooked feature of car and cdr is composibility, e.g. caadr, cddr, etc. Though I don't think it is useful to go ten deep as the spec requires if people are reading the code, deep composition may make some sense for machine written code.

Car and cdr also reflect a sometimes misunderstood aspect of Lisp: lists are an abstraction for sequential memory addresses in the Von Neumann architecture. The sense in which Lisp was designed for functional programming only goes about as far as one can pragmatically throw the lambda calculus. Practically and historically speaking Lisp programming was only slightly less about mutation than any other language. Design for current state of the art (in the 70's, 80's and 90's) is why Common Lisp has destructive versions of everything useful. Heck, Lisp even mutates its source code.

At the end of the work week, the major language design choice is not so much between car/cdr and first/rest it's between the language having a concept of first/rest and not having one: e.g. Python, Javscript, Go[?], C, and so on.

Finally, car/cdr is not that much worse than first/rest for all those programmers who are not fluent in English. Naming things is hard mostly because names are arbitrary. Both car/cdr and first/rest require the harder concept of fixed order data...and next makes more sense than first if one applies the stream abstraction.


> A sometimes overlooked feature of car and cdr is composibility, e.g. caadr, cddr, etc. Though I don't think it is useful to go ten deep as the spec requires if people are reading the code, deep composition may make some sense for machine written code.

The 90% case here is "second", "third", etc. For more exotic cases, surely there are other naming conventions that would be more readable. You could use "h" and "t" for head and tail, or "l" and "r" for left and right...

> At the end of the work week, the major language design choice is not so much between car/cdr and first/rest it's between the language having a concept of first/rest and not having one: e.g. Python, Javscript, Go[?], C, and so on.

The "concept of first/rest" is just the concept of pairs, which are a special case of tuples, which Python and JS certainly have.


Erlang uses a {H|T} pattern matching convention in lieu of language keywords. For what it is worth (limit x | x -> 0), I think Erlang is an exceptionally elegant piece of engineering as engineering. But it's not the 90% use case. Then again, neither is Lisp, ML, or Rust. The 90% use case these days is Javascript. Which I would hesitate to associate with the phrase "elegant piece of engineering" without an "in" prefix.

Python and Javascript may have pairs. They just live at the bottom of a Turing Tarpit.


I rarely used Python before the last few months, but I've already stumbled upon a great tragedy in the 3.x migration.

Although few know it, and fewer use it, Python 2.x allowed tuples to be unpacked in function heads, like Erlang. Since my last job was 100% Erlang, I stumbled into using it without knowing any better.

I decided a couple of months later that since it was a new project, I really should convert to Python 3, whereupon I discovered that feature had been removed due to parsing complexity and "because no one uses it."

Sigh.


This feature is still good to know about for use on legacy projects. Thanks for sharing!


With Python 2 due to be EOL'd in a couple of years, I'm not sure I should feel good about encouraging more use of it!


> The 90% case here is "second", "third", etc.

In addition to first and rest, Lisp has provided second, third, fourth, etc since the 70s, as well as the more general nth/elt which just takes the index as an argument.

> there are other naming conventions that would be more readable. You could use "h" and "t" for head and tail, or "l" and "r" for left and right

Why would you bother replacing names with 60 years of precedent with h and t so that instead of cdaddr you could write thtt? I think cdaddr nonsense is illegible and very bad style, but I don't see how someone could seriously suggest writing thtt as a good solution to that readability problem. The real solution is to use named accessors instead of ad-hoc pointer chains.


> The 90% case here is "second", "third", etc.

Unfortunately, those cases exhibit poor style if they are mixed with cddr. For instance, I would never write this:

  (when (and (consp (cdr x)) (consp (cddr x)))
    (do-something (third x)) ;; ouch!
    )
If we checked that cddr is a cons, we then want to access caddr (its car) or cdddr (its cdr).

Those first, second, rest and whatnot are really geared toward when the structure is a (proper!) list. If it really is just a proper list and we just want to be sure it has a third element, it would be more consistent to just do

   (when (>= (length x) 3)
     (do-something (third x))
Basically stick to one way or the other.


> car and cdr worked for Lisp because, during the '70s, Lisp as a whole was novel enough to gain a sizable following. It doesn't work today. (And note that, even then, Lisp lost a lot of potential users due to the S-expression based syntax.) I'm firmly in the "first" and "rest" camp

first and rest are bad names for what car and cdr do, in the general case. If you have "Jenny" mapped to "867-5309" in a dictionary, then getting the "Jenny" part of that binding with first and the "867-5309" part with rest doesn't really make any sense. Similarly, if you have a node in a binary search tree, getting the subtree with the lesser elements with first and the subtree with the greater elements with rest also doesn't make any sense.

Lisp, even in the 70s, uses first and rest as synonyms for car and cdr for the case where you are operating on a list. first and rest do not make sense as replacements for car and cdr in the general case, however, and if all you want is to have them as synonyms for when they do make sense, then Lisp has been providing that since the 70s.


If you want precise names to offer as aliases to "first" and "rest", then I think ML-style "head" and "tail" are better names than "car" and "cdr".


> If you want precise names to offer as aliases to "first" and "rest", then I think ML-style "head" and "tail" are better names than "car" and "cdr".

I don't know what you mean by ML-style, as most ML-derived languages call car and cdr fst and snd; I don't know any that call them head and tail.

But aside from that: really? I think of head and tail as being basically synonymous with first and rest (every language I know of that has built-in head and tail functions, or that idiomatically uses head and tail or abbreviations for variable names, uses them to mean the first element, and the list beyond that element. I don't know any that use them for two parts of a 2-tuple). The left child of a binary tree node being the head and the right side being the tail makes no sense at all to me. Not that car and cdr shouldn't be aliased when they're being used that way, but reading as "element a" and "element b" is better to me than "head" and "tail" which are equally meaningless, but by having English names imply meaning.

Lisp offers (since the 70s) the aliases first and rest for when conses are being used as lists, and I think those are fine and should be used when you are operating on lists. Conses can be used as other things than lists, though, and first/rest (or head/tail) don't work as meaningful names for any usecase aside from lists. Head/tail are totally equivalent to first/rest to me and I don't really care which pair is used for naming the list-handling functions, but they're both bad sets of names for the cons handling functions.


Head and tail make more sense in ML because data gets the stream abstraction. The semantics of car/cdr make sense in the context of Von Neumann memory. It's no accident that car/cdr came from machine code...and that's what Lisp was mostly competing with.


I don't see how head and tail make less sense as names for generic elements of a pair than abbreviations derived from idiosyncrasies of a long-dead computer from the 1950s.


Sorry for not being clear. Specific terms make more or less sense depending on the context for which a language was designed. The ML family of languages comes out of telecom where the stream abstraction expresses a primary aspect of the principle input. Production versions of Lisp developed as a substitute for machine code. Even today, Common Lisp is useful for abstracting over operations best understood in terms of places where values mutate. Functional programming ideology aside, Common Lisp provides a good tool for "close to the metal thinking". It's not as close as C. It's closer than Java on top of the JVM.

In ML, head/tail are a bit of training wheels anyway. Most interesting ML code relies on [x1::x2::x3::[y::ys]] pattern matching. Erlang (another telecom language but without ML's academic pedigree) foregos a formal head/tail and just relies on idiomatic {H|T} pattern matching. In ML head/tail make it easier to teach students the concept of car/cdr, but not much else.


> first and rest do not make sense as replacements for car and cdr in the general case, however, and if all you want is to have them as synonyms for when they do make sense, then Lisp has been providing that since the 70s.

And were it so that one really felt that LISP were incomplete without `first` and `rest`, wouldn't it be trivial to wrap them in custom functions (ie alias them)?

Don't get me wrong, but of all the things that will make learning LISP a bit of work, adding a couple of utility functions isn't on the radar...


> It doesn't take much experience (a week?) for the words car and cdr to mean the left and right halves of a cons cell to you.

A long forgotten five minutes sometime in Y2000 in my case.


When you see car and cdr in code, it is usually a loud and clear signal that this module is doing something very concrete and explicit, with conses as a structuring material.


I agree. The argument against car/cdr reminds me of the adage 'in theory theory and practice are the same and in practice they're different'. car and cdr may have begun as a historical accident but they stuck around because they hold up in practice.

As other commenters have pointed out, we could add to your list the classical point that car and cadr are composable, so cadr, cdar, etc. This creates a simple DSL for list manipulation which sometimes is just what's needed.


> 4. Their meaning is precise: car just takes the first element of a list. Whereas something called first might reasonably be expected to also give you the first element of a vector, or a string.

That may be true in LISP, but it's not true in Clojure (which I assume is a fair paragon for the first/rest camp), where first works just fine on strings and vectors:

  => (first "abc")
  \a
  => (first [1 2 3])
  1
> 5. When you're operating on lists as trees, the names first and rest are actively misleading. The car and cdr are the left and right subtrees, not the first and rest of something.

Firstly, trees can clearly branch out more than n=2. Then, nth becomes a much cleaner term than "left" and "right" which immediately limit you to 2 cases. But, let's say we're talking about n=2 trees. Consider:

  headmarc.core=> (second [1 2 3])
  2
And, of course, if that still seems unreasonable:

  (def left first)
  (def right second)
... but maybe what you really want is a zipper, which has everything you're asking for and more: https://clojure.github.io/clojure/clojure.zip-api.html#cloju...


TXR Lisp, a dialect with true conses:

  1> (car "abc")
  #\a
  2> (cdr "abc")
  "bc"
  3> (cddr "abc")
  "c"
  4> (cdddr "abc")
  nil
  5> (caddr "abc")
  #\c


I'm not sure if that's what you meant, but the point I was addressing was that "first" is bad because you could reasonably expect it to work on anything sequentialish not just on conses (agreed), and car/cdr is explicitly about conses (agreed). But first, in some Lisps, works just fine on everything sequentialish, including vectors and strings, so I don't consider that a particularly strong argument.

cl, cr I could live with, maybe. (consleft, consright). Other suggestions, head and tail, fst and snd, which have some of the other touted advantages. (I don't necessarily agree with them, but they also don't hurt, so...)


car and cdr is not necessarily about conses. I made them the basis for iterating over any list-like sequences in TXR Lisp. To participate in the protocol, an object can implement methods called cdr, car and nullify. This latter nullify should return nil if the object considers itself as representing "empty". Otherwise it can return the object itself, or possibly something else, like an ordinary list.


> They look similar to one another, which telegraphs that they're part of the same set of list access tools.

…and probably allows for more typos and misreads during code review or something else. Luckily 'a' is distinct enough from 'd' visually so the difference between them is more noticable; imagine functions ending up with unfortunate names like cbr/cdr or car/cer…


> When you're operating on lists as trees, the names first and rest are actively misleading. The car and cdr are the left and right subtrees, not the first and rest of something.

If by trees you mean 2-ary trees, sure. For any other n, #'first and #'rest are probably the correct generalisation.




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: