9 ms·
Fundamental Problems of Lisp, the Cons Cell (2024)
- chubot 1y agoWhy not Clojure? It's based on immutable maps and vectors, so I feel like cons should be used much less in idiomatic Clojure code
- kazinator 1y agoPretty sure this is not 2024. Oh look, it says so right near the top: 2008.
- lisper 1y agoThis author [1] has a fundamental misunderstanding of cons cells and what their purpose is. Cons cells are a primitive. They are best understood in contrast to the C model where memory is an array, i.e. an associative map from integers (or at least some abstract data type with a total ordering) onto values which are also integers (or at least sets of objects that can be mapped into finite sets of integers). The key difference between the C primitive and the Lisp primitive is that the Lisp primitive does not require anything to be mapped onto integers whereas C does. You have to be able to increment and decrement pointers in C. You can have a fully functional Lisp with no numbers at all. In fact, this was the whole point of Lisp when it was invented: providing a model for computing on symbolic expressions rather than numbers. There is a reason that the original Lisp paper was called, "Recursive functions of symbolic expressions and their computation by machine" rather than "Cons cells: a new computational primitive". All of the "problems" with cons cells are not problems with cons cells at all, they are problems with punning cons cells, i.e. using cons cells to implement higher-level abstractions without properly hiding the fact that the underlying implementation is a cons cell. The exact same thing happens in C when you run off the end of an array or cast a pointer to (void *). These are not problems with pointers per se, they are problems with using pointers to implement higher level abstractions (like finite arrays) without properly hiding the underlying implementation. Punning cons cells and abusing pointers have similar consequences: segfaults in the case of C, and (typically) "NIL is not a valid argument to..." errors in Lisp. Both of these are the result of bad abstraction discipline, not a problem with the primitives. --- [1] The author is Xah Lee, who is pretty well known in the Lisp community. He is widely considered a troll, an opinion which I personally share. I've tried to make my assessment of this post as dispassionate as I can, but I thought I should disclose the fact that I have a pretty strong bias.
- kazinator 1y agoIn TXR Lisp, I abstracted car/cdr to work with non-conses. $ txr This is the TXR Lisp interactive listener of TXR 300. Quit with :quit or Ctrl-D on an empty line. Ctrl-X ? for cheatsheet. TXR is written, directed and produced by, not to mention starring, Kaz. 1> [mapcar chr-toupper "abc"] "ABC" There is an important function and concept called nullify. This deals with the fact that empty nonlists are not nil. nullify returns its argument (like identity) if the argument is a non-empty thing. If the argument is an empty sequence, it translates it to nil: 2> (nullify "") nil Thus: 3> (cdr "a") nil 4> (car "abc") #\a 5> (car "a") #\a 6> (cdr "a") nil Here, nullify at work. (cdr "a") wants to be "", but gets nullified. 7> (car "ab") #\a 8> (cadr "ab") #\b 9> (cddr "ab") nil You can define a structure with car and cdr methods. Heck, rplaca and rplacd too! 10> (defstruct kons () bling blong (:method car (me) me.bling) (:method cdr (me) me.blong) (:method rplaca (me splat) (set me.bling splat)) (:method rplacd (me plonk) (set me.blong plonk))) #<struct-type kons> 11> (new kons bling 1 blong 2) #S(kons bling 1 blong 2) 12> (car *11) 1 13> (cdr *11) 2 14> (rplaca *11 42) #S(kons bling 42 blong 2) You can have a nullify method which informs the language how to nullify your object. Our above kons structure needs nullify if it is to be a complete emulation of conses that work as sequences. https://www.nongnu.org/txr/txr-manpage.html#N-F001A7B8 https://www.nongnu.org/txr/txr-manpage.html#N-F001A7B8 This is not the only way to make an object which serves as a sequence; you can instead (or in addition!) implement array-like indexing via the lambda and lambda-set methods to have it vector-like rather than list-like. The empty function and nullify are almost inverses; anyway they are implemented in the same spot of a 16000+ line file. The difference is that empty errors out on objects that are not iterable, whereas nullify takes objects of all types: val nullify(val obj) { val self = lit("nullify"); seq_info_t si = seq_info(obj); if (seq_iterable(si)) { seq_iter_t iter; val elem; seq_iter_init_with_info(self, &iter, si); return if2(seq_peek(&iter, &elem), obj); } return si.obj; } val empty(val seq) { val self = lit("empty"); val elem; seq_iter_t iter; seq_iter_init(self, &iter, seq); return tnil(!seq_peek(&iter, &elem)); } But, anyway, conses are good. TXR doubles down on conses.
- Jtsummers 1y agoQuoting Xah Lee: > The other fundamental problem in the language is its cons cells as its list construction primitive. > Lisp at core is based on functional programing on lists. I found the LISP I manual while thinking about this post and how to respond to some of the comments. An interesting paragraph from it, I think, is important to understand cons in the context of Lisp languages versus whatever Xah Lee is thinking of. From page 14: > Compositions of car and cdr give the subexpressions of a given expression in a given position. Compositions of cons form expressions of a given structure out of parts. [emphasis in original as underscored text] Note what is missing from this: Any mention of the word "list". cons, car, and cdr are primitives for the creation and manipulation (or navigation) of S-expressions. Some S-expressions are lists (in the "proper list" sense), but not all. This has been part of the language since the beginning and it makes sense for it to remain part of the language. In Common Lisp, if you want list specific functions, they do exist. first and rest are synonyms to car and cdr. You can skip cons entirely and use list and append and a few other functions in its place. There are also functions which are generalized to operate on sequences (which includes strings and vectors along with lists). https://www.mirrorservice.org/sites/www.bitsavers.org/pdf/mit/rle_lisp/LISP_I_Programmers_Manual_Mar60.pdf https://www.mirrorservice.org/sites/www.bitsavers.org/pdf/mi...
- smarks 1y agoThis is a very old argument. Xah Lee wrote the linked article around 2008, but previously he had posted "Why we have cons?" on comp.lang.lisp (and comp.lang.scheme) all the way back in early 1998: https://groups.google.com/g/comp.lang.lisp/c/T_Tozq2P53I/m/NB52fcDbGYMJ https://groups.google.com/g/comp.lang.lisp/c/T_Tozq2P53I/m/N... Lee's post seems more of a question, but to my eye it's a "why X" question that thinly disguises an "X is bad (or ugly, redundant, useless, confusing, counterproductive, unnecessary, etc.)" argument. Essentially though it's the same topic. The post elicited around 60 responses, among them this one from the late Erik Naggum: https://groups.google.com/g/comp.lang.lisp/c/T_Tozq2P53I/m/gB_I5sZxv3IJ https://groups.google.com/g/comp.lang.lisp/c/T_Tozq2P53I/m/g... which includes the following gem (or turd, depending upon your point of view): > All in all, the pairs notion is redundant. I hope you understand and appreciate what I have written above so the following does not apply to you anymore. you see, I wrote it all because I _really_ wanted to say that that sentence is the single most ignorant and shallow-minded line that I have ever seen in any programming language newsgroup or forum and I hope _never_ to see anybody display such utter disregard for the brilliance and genius of people who came before them just because they grew up in an age when "simple interface" is sneered and laughed at in favor of "simple implementation" so any dumb fsck can implement it right out of a "for Dummies" book. From where I sit in 2025, I have nothing to add.
- gudzpoz 1y agoSo I've been writing a JIT runtime for a Lisp dialect [0] for a while and got into the habit of feeling jealous of other languages with array primitives, especially after I learned what you can do with arrays or opaque lists (see V8 element kinds [1] for an example). And then I discovered this article (or complaint?) by Xah Lee. Aesthetically speaking, I quite like conses, in that it is powerful and flexible enough to hold both trees and lists. But performance-wise, I guess it's always the simplest things that are hard to optimize, and cons-style linked lists can't really compete with arrays [2], if you are aiming to get on par with V8. However, I still believe that, with a JIT runtime, it is possible to have some Lisp lists backed by arrays and am working on it [3]. Currently, =setcdr= causes most trouble for me and I don't know if the additional checks are going to ruin the performance improvement brought by arrays. But if things go well, we might get to see what conses could have cost us. [0] Emacs Lisp, in Java, with Graal Truffle [1] https://v8.dev/blog/elements-kinds https://v8.dev/blog/elements-kinds [2] https://en.wikichip.org/wiki/pointer_chasing https://en.wikichip.org/wiki/pointer_chasing [3] I am not aware of other Lisps doing this. Cdr-coding is the closest, but it is still a linked list (implicitly).
- hayley-patton 1y agoIf you want vectors, use vectors, elisp has them too as primitives. (I don't mean to suggest you don't know that, but still, you can just use vectors.)
- gudzpoz 1y agoI was writing from the aspect of an Lisp implementer, so that means it is not how I can just use vectors but about how existing code represents a sequence, and it is part of the job of the runtime to make it as fast as possible. From what I see from existing ELisp code (at least in the Emacs codebase), the idiomatic representation of sequences (fixed-size or not) is using cons lists. And it is not surprising: Emacs vectors are fixed-size and that makes it very inflexible and only suitable for a few things. This matters because, for example, if you want Emacs to compete with VSCode in performance, you eventually compares how idiomatic code performs. Note that how cons lists affect performance in real-world ELisp code remains unknown because it is yet to be benchmarked, but exposing the internals of idiomatic lists as conses does pose some challenges for an implementer aiming for further optimizations.
- alphazard 1y agoCons, car, cdr are fine for putting things together and breaking them apart. The problem is everything is boxed, and the indirection mechanism is conflated with the concatenation mechanism. What you really want is the ability to compose a value (concatenate 2 things together), but store it contiguously in memory. If cons did that then you could create true products of values, without any indirection. Then you need a different method for creating indirection. A function for each of C's malloc and '*' respectively. That would give you the control needed to create trees with arbitrary branching factors.
- Jtsummers 1y agoIf you don't want cons, you can use vectors and arrays in Lisp. Cons isn't the only data structure available.
- johnisgood 1y agoAnd often you want to avoid heavy cons-ing, too.
- behnamoh 1y agoOne Lisp that doesn't have this con (pun intended!) is Janet. I really like the language, but the documentation is lacking. Otherwise, it's a nice little language which compiles to small binaries we can share. It's image-based (!) which is refreshing given how Racket, Clojure, etc. don't support images (at least not that I'm aware of).
- wffurr 1y agoTook me some digging to figure out what you meant by “image”. Seems like a snapshot of the state after loading the file but before executing main.
- kazinator 1y agoIn the mainstream, what adopted the image-based approach is office suite application. What is a document, such as a spreadsheet or wordprocessor (or integrationo of those?) It's a bunch of objects and code saved to disk, revived when you open the document. If you need to explain image-based to people unfamiliar, that's not a bad analogy.
- pbohun 1y agoI recently started using Janet and enjoy it too. I think Rich Hickey got it right when he created a single set of functions to operate on all collections, which fixes most of what's mentioned in the article. Janet does a great job of copying what Clojure got correct. I've found the documentation for Janet to be fairly adequate. What are some parts where the documentation could be better?
- DonaldFisk 1y agoThe cons is integral to Lisp, and is used to construct the list, which is a purely functional data structure. Other languages also have lists, e.g. Prolog and Haskell (as well as similar pure functional languages). Saying that they're a fundamental problem in Lisp is like saying that objects are a fundamental problem in Smalltalk or that arrays are a fundamental problem in APL. If Xah doesn't like cons cells, there are other languages he can use instead. Lisp (as well as Prolog, Haskell, ML, etc.) programmers won't be persuaded to abandon cons cells or lists as they find them extremely useful.
- lelanthran 1y ago> Lisp (as well as Prolog, Haskell, ML, etc.) programmers won't be persuaded to abandon cons cells or lists as they find them extremely useful. Who said anything about abandoning lists? The argument appears to be that lists don't need to be built up out of two-element cells (i.e cons cells), and that Lisp, by enforcing `cons` cells as a way to construct lists, is seeing some limitations. TBH, I agree a little that that PoV - there is no need for exposing the implementation of the list to the programmer, much less forcing the programmer into using the implementation. The other problem with enforcing lists to be constructed of cons cells is one of performance; the default list in Lisp is a cache-invalidating monster.
- dwringer 1y agoAlso to be fair to Common Lisp, it has lots of support for sequences that aren't functional lists. Cons cells just happen to be convenient for representing things like the source code so they are prevalent. They're hardly a requirement for any algorithm's logical implementation.
- light_hue_1 1y agoNot only has Haskell "abandoned" cons cells, its lists were never cons cells at all. The type system forbids (a . 1) entirely. In Haskell there's a movement to get rid of much of the special syntax for lists. And a lot of code has moved on from lists. Many people never use them, I don't. What killed lists is the reality of their horrific performance. They were great 40 years ago. But memory and CPU speed have scaled with different factors that make lists almost always the wrong choice.
- dragontamer 1y agoWait, what's the problem with Cons? This seems to just complain that cons is hard to understand or something, without actually saying what makes it hard or unintuitive. -------- Cons is fundamentally a linked list. The "pair" is a (value+pointer), and sometimes "value" can itself be a pointer. So when you have (pointer, pointer), you now can create trees. As such, "Cons" is a powerful abstraction for trees, pairs, and linked lists. That's all it is. If you need "more" than this (ie: graphs), then perhaps its insufficient. --------- "Cons" itself is a malloc(sizeof(pair)) operation. While car/cdr depend on whatever you're doing or however you've designed the program. If I were to criticize "Cons", its that high performance modern programming needs to use arrays because the cost of pointers has grown. Latency of memory lookups is relatively slower on modern machines (or really, latency hasn't improved in like 20 years), so you need to consolidate more members together. But in terms of a "simple" data-structure, "cons" cells and list-programming is a thing for a reason.
- KerrAvon 1y ago> If I were to criticize "Cons", its that high performance modern programming needs to use arrays This has been true of linked lists since the advent of memory caches; it’s not just the last 20 years. They’re simple to implement, but they’re usually very bad for performance; you really want better locality of reference in a primitive data structure. You almost always should be using an array of some kind instead.
- dragontamer 1y agoArrays are much worse at insertions, especially at the middle or beginning. There's a reason why the Linux Kernel continues to use Red-Black trees today for a huge number of its data-structures. When you need reliable performance on all operations (insert, delete, reorderings, reallocations), its hard to beat a tree (or "cons" for that matter). If you can use an array, then use an array. But making the tree a "good default" seems like the right move. Lisp does that with cons/cdr/car and (a . b), or (a b c d e) syntax. Turning (a b c d e) into (a b b1 c d e), especially for larger sizes, will likely be faster than arrays (which innately needs to memcpy ~half the array on the average).
- noosphr 1y ago>Lisp at core is based on functional programing on lists. Lisp is not, nor has it ever been, a functional programming language. It had lambda decades before other languages figured out it was a good idea, but it had set! for just as long. It is also not a list based language, but a tree based one. The fundamental point of lisps is that you can manipulate the abstract syntax tree directly since it's pretty much the same thing as the parse tree. >Deep Nesting is Rare >The lisp's cons [...] works just fine when data structure used is simple. [...] vast majority of list usage is just simple flat list, sometimes 2 level of nesting [...] 3 levels of nesting is seldom used [...] Greater than 3 level is rarely seen. Systematic manipulation and exploitation of nested list, such as mapping to leafs, to particular level, transposition by permutation on level, or list structure pattern matching in today's functional langs, etc is hardly ever to be seen. I guess the author has never used lisp to implement a DSL in lisp. Writing a lisp interpreted in lisp is the first real program you write if you take a lisp course at university and you need to handle arbitrarily nested expressions. Real world usage also involves writing a specialist language to solve your problem for anything more complex than a utility script, e.g. Emacs is a text editing DSL implemented in elisp.
- golly_ned 1y agoThe author is reknowned for eMacs lisp.