The Evolution of Lisp (1993) [pdf]
redirect.cs.umbc.edu
redirect.cs.umbc.edu
A Pattern of Language Evolution, Gabriel/Steele 2008, https://www.dreamsongs.com/Files/PatternOfLanguageEvolution....
The Evolution of Lisp (1993) [pdf] - https://news.ycombinator.com/item?id=14937424 - Aug 2017 (17 comments)
The Evolution of Lisp - https://news.ycombinator.com/item?id=3528694 - Jan 2012 (4 comments and a video re-enactment from 2008: https://www.infoq.com/presentations/Lisp-Guy-Steele-Richard-...)
The Evolution of Lisp [pdf] - https://news.ycombinator.com/item?id=2082417 - Jan 2011 (1 comment)
Forth has a similar quality. The reductionist sweetspot of "implemented as a stack language" and "one step up from machine code"
C is the "no guide rails" conversation. Some would say it's one step down from assembly because it has none of the rigour and only some of the speed. That lack of rigour, the ability to cast within a memory region if you know how your structure maps into that region, I find extremely useful. Pascal's version of it was more clumsy but that said, perhaps the semicolon (and full stop) alone make Pascal part of the C family.
(you can't directly compute over a value in both float and int in assembler if you are in the ALU/FPU register space. You do "cheats" which are not rigorous, but more rigorous than C)
Racket has all of that and everything else you need to get started like tooling and modules. [2]
Both lisps are no nonsense scheme variants.
[1](https://www.scheme.com/tspl4/) [2](https://racket-lang.org)
Here's mine, https://github.com/agzam/awesomewm.d/blob/main/src/_init.fnl
Beware though, I'm not highly experienced in using and configuring Awesome (especially for beautification), the code is not meant to be shared and it has tons of dirty parts (result of my experimentation), I only built what I needed for my personal use.
Bigger (more public) project built on Fennel is https://github.com/agzam/spacehammer It's for Hammerspoon though, and my guess you're using Linux, not Mac.
One of the aspects I still haven't figured out is the "proper Fennel REPL", it makes certain things less joyful to write or troubleshoot, but even without it, I see some benefits in using Fennel instead of Lua - it's more compact, more FP-oriented, has macros, and it's a Lisp - I can use structural editing.
by comparison touretzky is a college level slog, solid, dependable coursework, that takes you over all the proper subject matter in a way that's certain to be approved by any computer science department committee, but that fails to create any sort of motivation to engage.
Next, read An Introduction to Programming in Emacs Lisp. It's a lovely book and fun to read: https://www.gnu.org/software/emacs/manual/html_node/eintr/in...
Learn to read and work with Lisp by reading the Emacs source code and playing with it.
Check out the Evolution of Emacs Lisp. It's a neat little Lisp! https://dl.acm.org/doi/10.1145/3386324
Emacs has the best documentation and documentation system ever made. Learn C-h f, C-h v, and friends, as well as the info system and the power of indexes.
I hope you find joy and wonder exploring Emacs and Emacs Lisp like so many others have for nearly 40 years!
https://rosettacode.org/wiki/Category:Common_Lisp
This has a lot of problems solved in multiple languages.
Cheryl's birthday https://rosettacode.org/wiki/Cheryl%27s_birthday
Seems like part of the problem is that Lisp is often demonstrated with examples that your average programmer can turn to 'black box' pre-built functions/packages/software for. That Lisp can help one extend a programming language to add a CASE statement, for example, is not going to win the hearts and minds of those who are already using a programming language with a built-in CASE statement. It may be neat from an intellectual standpoint, but not from a practical one.
And the merits of the example linked to below of how Lisp can be used to implement an in-memory database will surely be completely lost on anyone with access to in-memory database software like DuckDB.
Practical Common Lisp: An MP3 Database https://gigamonkeys.com/book/practical-an-mp3-database
DuckDB: SQL Introduction https://duckdb.org/docs/archive/0.8/sql/introduction.html
If you want a "Lisp Aha! moment" you'd probably be better off looking at something that's actually Lisp-centric like Paradigms of AI Programming by Norvig (https://github.com/norvig/paip-lisp). You eventually build up to a Prolog to CL compiler written in CL.
You have to write code and experience it.
I mean, of course it has same cool constructs, but, at least for me, its the experience of writing code with it that makes it shine. And that kind of thing is intangible.
The iterative development experience is part of it. But, at least, the code I write has a rhythm to it, and I don't write other languages the way I write Lisp. Even at the 10,000 foot level, it's mot the same. For example, I tend to write more, smaller functions. Like Forth, its very easy to refactor Lisp. Being iterative and interactive, it supports and encourages that kind of thing.
Write a little thing, test it. Write another little thing, building on all the other little things, test those. (Note, not "write tests for them", just...test them. In the listener. "Good! Moving on!")
Find something that interests you, preferably something that's not "let's glue all of these other libraries together". Something that's going take 500-1000 lines. And, just write. Learn as you go, don't worry about being idiomatic.
See if anything pops from that.
I'd recommend checking out videos of people developing systems in lisp. Off the top of my head:
- https://www.youtube.com/@CBaggers/playlists
- https://www.youtube.com/playlist?list=PLTA6M4yZF0MzsMlNL0N67...
It depends on what you're trying to get out of it. For example, you can grab a simple code of some React components with JSX and ask ChatGPT to write it in Hiccup (Clojure). That would show you how s-expressions can reduce the syntax boilerplate and make it smaller. You can do the same thing with Lua and Fennel - Fennel code would be much shorter and cleaner.
However, without proper understanding and using structural editing and connected REPL, you probably won't see any benefits right away. Lisp code is not the best to be read off the "dead medium" - paper or screen. It's better when you have a connected REPL because it allows you to evaluate every single expression on the fly. Instead of trying to understand what (foo x y z) means, you can evaluate it and immediately see the results. With a connected REPL, you can evaluate them separately (individually), or together. Good talk of relevance: "Stop Writing Dead Programs by Jack Rusher" https://www.youtube.com/watch?v=8Ab3ArE8W3s
Next step in understanding the benefits of Lisp code is learning structural editing. Because everything is a symbolic expression, you can easily move, transpose, change, and replace parts of your program. Instead of rewriting a function of seemingly arbitrary syntax constructs, you'll be dealing with well-structured forms that you can easily move around. No, it not the same, better or worse than using refactoring features of your IDE with other (non-lispy) languages. It's just different.
These two aspects - REPL and structural editing - are really difficult to explain; one has to experience them in person to see if that way of writing software feels appealing to them. And yet, without grokking the basics of these two essential things, IMO, the Lisp experience will not be the same, and you may not get why some people get really excited about Lisps.
And If you're truly looking for a brain-bender, check out https://github.com/hyperfiddle/electric. Dive into the concepts, play with the example demos, and see where it takes you. But be warned: it's an incredibly dense project and definitely not for the uninitiated. If you haven't written in Clojure before, chances are you won't fall in love with its ideas overnight. But it's a good example of "practical applications" that are difficult to achieve unless you're using a Lisp.
"Most beautiful program ever" is short but the associated yapping takes almost 2 hours. https://www.youtube.com/watch?v=OyfBQmvr2Hc
From an interpreted Lisp system to a compiler it's mostly a slog of studying compilers and assembly (or machine code) and possibly translating the interpreter, which you can use to bootstrap the compiler implementation if you'd like.
It's common path, see https://mpov.timmorgan.org/i-built-a-lisp-compiler/ and https://bernsteinbear.com/blog/compiling-a-lisp-0/ .
The book also uses something more like fexprs than macros, which I don't have an opinion _outside of this book_ on with regards to language UX, but fexprs are generally harder to compile. That is in part because they control if/when their arguments are evaluated, but the fexpr-alike things here do not (instead "Q-expressions" delay evaluation like QUOTE). You still would want to do something about calls to EVAL though. The fexpr system _in this book_ also does have bad UX: it only incidentally works with dynamic binding (Shutt's lexically scoped fexprs explicitly pass the environment around) and it also leaks every variable binding from the fexpr function into what it evaluates.
The book also just doesn't cover compilers, so you're doing that slog on your own.
(I found this while writing this comment: https://bernsteinbear.com/blog/lisp/12_metacircular/)
Some variants just drop that part and focus on the language only. Clojure and Racket are in that category. They lack the full power of CL; so they're just Lisp-syntax languages, not an actual Lisp.
Maybe this high barrier to entry is why Lisp hasn't caught on even after 30 years of CL standardization. People often don't want to learn a new IDE to get the most out of Lisp. And if they stick to their current IDEs, they only get a glimpse of Lisp through its syntax (and much limited macro system). At that point, it's questionable whether whatever remains of Lisp does indeed provide any advantage over more mainstream languages (e.g., instead of learning Clojure, why not just learn a functional language like Elixir or Haskell? It's not like Clojure gives you the full Lisp power anyway.)
Addendum: As one of the comments below mentioned (thank you @lispm), what I meant by "dropping the full power of Lisp" is the ability to modify a running program (continuation/restart system). Does Clojure REPL drop you in a new REPL in case of errors so you can literally change the running code or enter value for a missing variable?
This very side is written in CL in vi, if I'm not mistaken?
As an aside, please remember the guidelines: 'Please respond to the strongest plausible interpretation of what someone says, not a weaker one that's easier to criticize. Assume good faith.'
In retrospect, the comment was talking about Common Lisp and not just Lisp-likes and tried to make the distinction but did a poor job of it with comments like:
> Clojure and Racket are in that category. They lack the full power of CL; so they're just Lisp-syntax languages, not an actual Lisp.
Then there is another school which considers the meta programability, REPL, syntax, ... the more philosophical notion of Lisp (Scheme, Clojure, Racket, Arc, ...). Some have no similarity with Lisp 1.5 descendants, and only share a superficial similarity.
Lisp seems to be the only language where people really want to argue that they are a Lisp, nobody tries to argue that Javascript, Java, PHP etc are "a C" (similar syntax? semantics?). Does it even matter what is "a Lisp"?
The barrier for Lisp (be it one school or the other) is about as high as Python, you type the name of the implementation and do stuff. CL specifically doesn't say anything interesting about the REPL (e.g., SBCL doesn't even allow by default to navigate forms on the command line), so to say that it is "powerful" is very misleading. Similar about the requirement of Emacs, you can use anything you like.
Lisp is far from being the only family of languages. There's the APL family, with k, j, q. There are the many Forths, to the point where it's been quipped that the language has more implementations than users. The C family rather clearly includes C++, Objective C, and D, as well as C itself (Swift and Rust are arguable here).
I wouldn't put the three languages you named in the C family, but they're all Algols in a manner of speaking. At least, I routinely describe them that way, and people tend to understand what I mean. The reason no one argues that those languages are in the C family is just the broad and informal consensus that ABI compatibility is the most important feature, rather than syntax.
Any time there's taxonomy going on, you'll have lumpers and splitters. I'm a lumper where Lisps are concerned. I would call Dylan and Julia Lisps, and have done so.
If anything, what makes Lisp unique here is a faction of Common Lisp users who feel strongly that the only Lisp is Common, and historical variants which lead up to the Hyperspec (elisp being grandfathered in on that basis). I've never entirely understood this, but I don't let it bother me. That appears to be the source of the argument you reference in the section of your post I've quoted.
The point is not "only family" its "only family, where people argue about being in the family"
I think it's actually not the only language family where people argue about admission. But that doesn't seem to be your point.
Your point seems to be "there are other families"
Much of my reply was a response to the other clause I quoted, about no one arguing that JS, Java, and PHP, are "a C". I pointed out that the reason for that is simply that no one thinks of them that way, superficial syntax resemblances aside. There is in fact a C family, but it happens that you picked three languages where neither users of C family languages, nor users of the languages you mention, would argue that those three are a part of it.
You'll note that arguing for vs against is not a disagreement: if someone is arguing against inclusion, that rather implies that someone is is arguing for it. Rather, your last sentence posed a question, based on your observation. My reply, which explains why there's such an argument to begin with, was intended to answer it.
Yes, it matters in the following way. New languages being touted as Lisps, which deviate from the Lisp characteristics, cause confusion.
For instance, nowadays you read comments like: "I tried Lisp for a while but couldn't get used to doing everything using immutable sequences, nice as the pure functional paradigm might be conceptually". Obviously, that person is referring to Clojure, and since Clojure is promoted as a Lisp, they believe that all that is true of Clojure is true of Lisp.
People believe such misleading statements which is then harmful to the real Lisps. Someone out there will avoid using Common Lisp believing that they would be required to do everything using immutable sequences or hashes.
Incidentally it's MORE confusing for people to say Clojure is not a LISP.
Clojure is a dialect of Lisp, and shares with Lisp the code-as-data philosophy and a powerful macro system. Clojure is predominantly a functional programming language, and features a rich set of immutable, persistent data structures. [...]
It would be helpful if this said something like:
Clojure is a dialect of Lisp. Though sharing with other dialects the code-as-data philosophy and a powerful macro system, it is very different from the original LISP of John MacCarthy, and the subsequent mainstream languages like Common Lisp and Scheme that are in contemporary use. Unlike these, Clojure is predominantly a functional programming language, and features a rich set of immutable, persistent data structures. [...]
Obviously the author considers Clojure a lisp and I dare say Rich Hickey knows what he's talking about.
I brought up the functional point because it's ridiculous to say Clojure is confusing people into thinking all lisps are functional. There's no reason to think that
Yes?
> something pedantic most people don't agree with
How so?
> Obviously the author considers Clojure a Lisp
The revised paragraph still insists that Clojure is a dialect of Lisp, like before.
> ridiculous to say Clojure is confusing people into thinking all lisps are functional
I've witnessed it first hand a number of times now, from people whose only exposure to the Lisp world is Clojure.
>> This paragraph is something you wrote
> Yes?
I don't know why you're chopping my sentences in half to have fake points to respond to. I was saying the paragraph was written by you and for you. It is not helpful for beginners trying to get into a language to read random names and other languages thrown at them, and be told "It's like this other language you don't know about, but not quite." That's how you get people clicking away from your site.
It's just a weird hill to die on to say there's a problem with calling Clojure a LISP because it will "confuse" people. It doesn't confuse people that know what LISP is. Beginners are always going to be confused, that's par for the course.
https://en.wikipedia.org/wiki/Clojure
I believe I've said this to you before in the context of Scheme: the Common Lisp community doesn't get the final vote as to what is or is not a Lisp.
As to your example, it's at least as likely that someone would be turned off Lisp by using Common Lisp, and someone else might suggest Clojure as a remedy to whatever issues they had with it.
But that's at least a consistent situation. That user experienced an example of actual Lisp and was turned off.
> suggest Clojure as a remedy
So, there it helps Clojure to just be Clojure and not to identify as Lisp, what the user was turned off of.
- Code as Data? Check
- Homoiconicity? Check
- First-class Functions? Check
- Macro System? Check
- Automated memory management? Check
- REPL-driven interactive development? Check
I don't know what else do they want. I feel like this is some kind of "programming language racism" and "Lisp eugenics" bullcrap.
Yes, Clojure is different than Common Lisp, but it is important to recognize that diversity in languages and their ecosystems can be valuable, and differences don't diminish the contributions or identity of a language within the broader Lisp family. Why can't we instead borrow good ideas from Clojure and implement them in CL and vice-versa?
I love Lisps. I love Common Lisp and Clojure, Fennel and even Emacs Lisp. Whenever I need to jump between Javascript and Typescript or even between different frameworks, I feel like I'm programming in different languages. That doesn't happen with Lisp. I can relatively easily switch between Clojure, Clojurescript, Clojure-Dart, Fennel, Emacs Lisp and CL and still feel like I'm programming using the same language. Completely different platforms - same language. I really don't understand what this fuzz is about. Clojure is a Lisp.
I think that the lack of an interactive experience is not necessarily a deal breaker. We could have a Lisp implementation which compiles to an executable image which doesn't run until uploaded to a target. This could use an existing dialect like Common Lisp or Scheme and be used by Lisp programmers to deploy some of their existing programs.
If you were to fix your rules to be more inclusive, it would be more consistent with your diversity rhetoric. (Not saying you should do anything; your rules are your business.)
These are not "my rules," I didn't make up this shit. Many renowned computer scientists have spoken about Clojure and at Clojure conferences: Guy Steele, Paul Graham, Matthias Felleisen, Gerald Jay Sussman, Daniel Friedman,... [some names just off the top of my head]. I'm pretty sure, if not all of them, most of them would definitely say "Clojure is a Lisp". So, until someone of that caliber steps onto the stage and tells us differently, that's what I'm calling it.
People don't argue whether Javascript is "a C". However, people do argue whether GNU C is C. Some pedants insist that only ISO C is C and that, for instance, GNU extensions like nested functions are not C. (And are off topic in the usenet newsgroup comp.lang.c.)
To me, C++ is a dialect of C. In fact, you can write production code in a language that compiles as either C or C++ and behaves the same way, with similar executable size and performance. When you do that, there are obviously C++ features you cannot use, but also C features you cannot use.
Not true from long ago. By far, that's statement it's pretty wrong. You will get zillions of bugs and compilation errors in both ways.
C23 is closer to C++ in one small way: () is now a prototyped parameter list, meaning the same as (void). Thus if you want to program in a common C23/C++ dialect, you can drop the (void) kludge now.
That's pretty debatable that Clojure and Racket lack the "full power of Common Lisp". For one, they're all three different languages, each one having something the other doesn't. And another is that Racket is not a Lisp and doesn't purport to be. It's very much in the Scheme camp, but even then it is clearly it's own thing.
That said, unlike COBOL, most of whose programmers these days use Visual Studio Code or a modern IDE, Emacs is still the best readily available support one can get for Lisp programming -- and that's kind of a black mark against Lisp. I really wish Allegro or somebody would deliver a modern IDE for an affordable price, or at least good VSCode plugin support.
> why not just learn a functional language like Elixir or Haskell
Have you ever considered that specifically because Clojure borrowed some ideas from Lisp, some from object-oriented languages, and some from FP, it makes it a really good choice for certain applications and developers?
Besides, is it really a big deal not to have proper continuation/restart in Clojure? It really depends. For many use cases, the error handling techniques in Clojure are adequate and align well with its functional paradigms. I've been using Clojure for several years and haven't heard anyone seriously complaining about the lack of continuations, despite many former Common Lispers now being full-time Clojuristas. There are also some philosophical differences: The philosophy behind Clojure focuses on simplicity and building software with different tools and techniques, which can achieve similar robustness in practice. So, I don't know. Sure, I personally wouldn't mind having built-in support for continuations in Clojure, and there were some attempts to bring that via libraries, but this was never a "high priority thing" for Clojurists. If in 17 years of existence of Clojure (and almost 40 years of Emacs Lisp), that never became a thing everyone wants, I guess it's not as critical as you paint it to be.
A language is not only its syntax, but also it's semantics. Lisp is not just parentheses or even 'homoiconicity.' In addition to the surface syntax, there is a shared cultural understanding of what certain symbols mean, how they fit together into a system, what a symbol even is, and so on.
Given that, while there is some surface resemblance to lisp, it's plainly obvious to me that Clojure does not belong in that family. This is not a dig at Clojure! Clojure makes a lot of people happy and productive, and that's great. It's my third favorite language on the JVM. But when you take a closer look at some very core design decisions made in Clojure you'll find that they depart strongly enough from all the other languages in the Lisp family that it belongs to a distinct language family. A few of them:
- Reader doesn't intern symbols
- EQUAL symbols aren't EQ
- Introduction of vars
- Cons doesn't cons
- No numeric tower
- Backquote behavior
These things don't necessarily come up in day-to-day programming, so it might seem like quibbling, but these are fairly central differences in meaning.
I think it's sensible to consider "Clojures" as a distinct language family. Rhere are languages like Fennel and Hy that are clearly Clojures, and I'm sure there will be more.
But even the "dialect of Lisp" actually is more like "spiritual dialect of Lisp".
Clojure was from the beginning designed with exactly ZERO backwards compatibility to Lisp. Exactly ZERO of the existing Lisp programs/libraries could work. It was even difficult to port them. It's basically a kind of "dialect" with new operators and data structures, which interprets prior concepts in new ways and mixes in new stuff.
But it was designed with compatibility to Java and the JVM -> one could not reuse existing Lisp code, but one could reuse existing Java code -> since Clojure was hosted on the Java and had integrated calling Java code. Reusing Java was more important, because the goal was to work in a Java industry. Thus there wasn't a version of Clojure on top of Lisp, only versions on top of languages/infrastructures like JavaScript or .net, because there was another source of reuse.
If we look at the above linked paper "The Evolution of Lisp", then "Evolution" actually meant "Evolution", not "Restart" -> the language evolved by improving, adding, extending, removing, ... around a core design.
Clojure was designed as a new dialect, free of the baggage from the past, with a completely fresh start. Reusing existing code or evolving an existing language was a non-goal. It was newly designed as a hosted implementation (-> the Lisp runtime was gone), it was designed with lazy data structures at its core (-> the Lisp linked lists were gone), it was an opinioned new design, based upon of what Rich Hickey liked about Lisp and what would help him to be employed as a consultant in a Java world -> by developing upon and within the Java stack of standards, tools and libraries.
And that's fine for him. Rich Hickey had his goals and succeeded with them. Other people found it useful, too.
Btw., even though "lernen" and "learn" are coming from a common language background (Proto German, also called Common Germanic, no joke -> https://en.wikipedia.org/wiki/Proto-Germanic_language) and here mean the same.
There's no confusion. Anyone who wants to learn Lisp can start with Emacs Lisp, Racket, Guile, Janet, Fennel, Clojure, or Common Lisp, and soon they'd know how they differ. Telling people "Oh no, Clojure is not an actual Lisp" is disingenuous. Yes, there are certain differences, but the main ingredients are there - homoiconicity, macros, REPL-driven. Sure, in some contexts, clarification is required, and usually, it is present. I have never heard anyone say something like "I actually wanted to learn Lisp, but I ended up using Clojure and never found out what 'the real Lisp' is like." When people want to specifically talk about Common Lisp, they say that. Again, I have no idea what the fuzz is about, Clojure is a Lisp. Yes, it's not Common Lisp, it's not Emacs Lisp, we know that. Most people who know just a bit about Lisps, do know that. And yet we're arguing like someone accidentally may end up using wrong Lisp and kill thousands of polar bears or something.
Cons cells and linked lists are not completely gone, though:
user=> (cons 1 (cons 2 nil))
(1 2)
Cons cells however are somewhat limited in that the second argument to cons must be an ISeq. user=> (list? (cons 1 '(2 3)))
false
user=> (cons 1 '(2 3))
(1 2 3)
That's "strange", isn't it? It prints as (1 2 3), but it is not a list? So there are things which print as lists, but aren't lists? What? Which also means that when we print it and read it back, it will be of a different type and list? will be true?! ELISP> (listp (cons 1 '(2 3)))
t
Puh, Emacs Lisp got it right.Like declaring some mini-C 'ANSI C compatible' and you can't even provide a complete but basic stdio.h header.
Have you read the docstring for (list?)? It says "Returns true if x implements IPersistentList". (cons 1 '(2 3)) results in a clojure.lang.Cons not the instance of clojure.lang.PersistentList. A clojure dev would be using (seq? (cons 1 '(2 3))) instead of what you tried.
Maybe before hating something so fervently try to learn it a bit more first?
Well, no kidding.
> (cons 1 '(2 3)) results in a <Java thing> not the instance of <another Java thing>.
:)
Clojure encourages using idiomatic patterns that align with the host platform’s best practices. For instance, when running on the JVM, it leverages JVM's threading model and garbage collection; in Clojurescript, it adopts patterns suitable for JS's event-driven model.
Clojure does not try to provide a uniform abstraction layer that masks platform differences. Instead, it exposes platform-specific capabilities, making developers aware of and able to exploit the unique features and strengths of each platform.
Clojure compiles down to the native code of the host platform. For the JVM, it compiles to JVM bytecode; for JS, it compiles to JS code. This means the compiled code runs as if it were natively written in the host language.
Clojure operates within the host runtime, using the host platform’s execution model, memory management, and runtime services.
Being a hosted language means that Clojure does not attempt to hide or abstract away the platform it runs on. Instead, it integrates deeply with the host platform, leveraging its native capabilities and interoperation features. This allows Clojure code to be idiomatic to the host environment and benefit fully from its strengths.
Moreover, you can have namespaces where Clojure and Clojurescript code is intertwined. I have never seen so much code reusability, even when I was heavily writing for Node. Check this out: you can write specs that would represent your data, alright? Then, you can generate some random collections of data based on those specs. The data that you can use both - on front-end (e.g., for end-to-end testing) and back-end (e.g., for testing the domain logic). You can also use the same specs for validation in UI controls - 'not a valid email', etc. Neat, right?
Other Lisp dialects run on top of the JVM, .net, JavaScript, LLVM, UNIX, iOS, Android, Linux, ... and there is no such problem.
Try ABCL on the JVM. (listp (cons 1 '(2 3))) is T.
I would think that consistency and correctness of basic Lisp operations (-> LISP stands for "List Processor") would be a high priority of any self-respecting Lisp dialect?
To be honest, I never knew about this (list?) thing behaving differently with (cons) because I personally have never stumbled upon that. Most likely, people raised this question because, like I said, the language has been around for decades. I don't know the proper and accurate answer for you here, that's probably some intentional design choice rather than a bug, driven by the need to balance consistency with platform-specific optimizations and constraints; if that was important, I would've found it and known how to explain it. It is not important (think of it as an easter egg), because...
Like I said before, Clojure emphasizes using seq abstraction instead of lists. It is a core part of Clojure's design and works uniformly across Clojure, Clojurescript, Clojure-Dart and other variants. It abstracts over different collection types, allowing you to work with sequences without worrying about the underlying concrete types. Functions that return `seq` often produce lazy sequences, which can be more memory-efficient and allow for incremental processing. This is particularly useful for handling large datasets or infinite sequences. `seq` functions, such as `first`, `rest`, `next`, and higher-order functions like `map`, `filter`, and `reduce`, provide a consistent API and behavior across different implementations of Clojure. This minimizes the impact of platform-specific differences. In addition, it can often lead to performance optimizations tailored to the specific platform, as it allows the runtime to use the most efficient way to traverse collections.
Yes, Lisp traditionally stands for "LISt Processing," and lists are indeed central to Lisp's heritage. However, modern Lisp dialects have evolved to handle a broader range of data structures efficiently while maintaining the core principles of Lisp. Common Lisp, while having first-class support for lists, also has a broader set of data structures including vectors, strings, and bytevectors. Racket enhances the language with a diverse range of data structures—vectors, hash maps, sets, and streams. Elisp, too, has hash tables and strings. Clojure adds vectors, maps, and sets as first-class citizens. I guess that's why we no longer use "Lisp" as an acronym - we no longer capitalize each letter. I guess one could say "it is still 'LISt Processing' because every expression written in it is a list..." ¯\_(ツ)_/¯
This is a programming system, which is a list processor. Not a vector processor, not a graphics processor, not a number cruncher -> it's a list processor.
Now McCarthy defines three basic LISP features:
* Data has the form of S-expressions, for which a definition is given, which is linked lists of atoms&lists. -> a very low level definition.
* There is a LISP language which processes these s-expressions. Simple operators are being defined.
* LISP can interpret and execute LISP programs in the form of s-Expressions.
To sum up: there are linked lists, a programming language for processing them AND the programs themselves can be interpreted by LISP.
He then defines an "universal LISP function", an interpreter for LISP in LISP.
That's the actual core essence of LISP. Think about it!
That's the big thing which was defined in the very first pages of the first LISP papers: a very simple foundation, which describes how to do computation by a List Processor, applied to itself.
This is the core language: a data structure, a few operations and an interpreter written in itself.
Fire up your Emacs Lisp (start GNU Emacs and do m-x ielm). It's all there: the data structure (-> linked lists of symbols), the operations (car, cdr, cons, read, print, cond, lambda, eq, assoc, append, ...) and the interpreter (-> eval).
It's not one of many data structures, it is the central thing: s-expressions, programs as s-expressions, a list processor (-> EVAL) defined in the language it is executing, executing programs which are given as s-expressions, able to execute itself.
All the other stuff you've mentioned is secondary: Homoiconicity, First-class Functions, Macro System, automated memory management, REPL-driven, interactive development.
McCarthy described a specific way to do computation. That's the core of LISP. One which can be taught in a day, which fits on a few pages and which allows us to develop a simple mental model of it.
Clojure does not have the same axiomatic definition of a List Processor, it is not interpreting lists, ... It has a complex data structure at its core: lazy persistent sequences, it compiles code (the compiler is written in Java and compiles to the Java Virtual Machine), it has the JVM underneath, ...
If I break the execution of a list processor, I see the programs as lists being executed by a lisp processor. Write a program in Emacs Lisp, execute it by the interpreted EVAL, run it, interrupt it and you'll see a backtrace where the interpreter runs the Lisp expressions.
If I want to teach people Lisp, I explain this simple LISP PROCESSOR. That's the mental model they need to have: the form of Lisp programs and its execution. Once they have this mental model of LISP, it is valid for all core LISP dialects.
This is also explained in the LISP literature: "LISP 1.5 manual", "Anatomy of LISP" (John Allen -> the book is available here for download: https://dl.acm.org/doi/10.5555/542865 ), "Structure and Interpretation of Computer Programs" (Abelson/Sussman), "Paradigms of AI Programming" (Norvig), "Lisp In Small Pieces" (Queinnec), "The roots of LISP" (Graham) and others. All these explain this foundation (and expand on it).
Clojure is slightly different. It's a modern&pragmatic&opinionated dialect of Lisp for the JVM, which does not have this simple LIST PROCESSOR. That's why it is an dialect of LISP, but not one which shares the axiomatic core of LISP.
Many classical Lisp implementations also compile code. Compilation does not preclude a language from being a Lisp. Clojure continues the use of s-expressions for code and data representation. Clojure provides a REPL, a staple of Lisp environments.
Clojure retains the homoiconic nature of Lisp. Macros in Clojure very much akin to those in traditional Lisps.
Clojure’s design choices are about extending Lisp principles to solve modern software engineering challenges. Rich Hickey wasn't like: "Lispers were all wrong..." He chose to build the language on top of Lisp principles, and these principles are evident, unmistakably clear and can be seen with a naked eye.
I'm honestly getting worn out by this debate. You're delving into the minutiae of taxonomy, like whether early hominids can be classified as humans, and it's just exhausting. I've told you before, and I'll tell you again — unless someone truly eminent persuades Rich and he eventually concedes, saying: "Yes, Clojure in reality ain't no Lisp..." only then will I alter my position. Until that time, I will continue to call it a Lisp, regard it as a Lisp, and talk about it as a Lisp. The only thing you can do to change my opinion - is to persuade Rich to change his.
LISP has dialects which are not homoiconic, where programs are not written as s-expressions, ... But the core is the same: a specific List Processor. McCarthy himself did not want to write programs as s-expressions. He defined M-Expressions, where S-Expressions were only used for the data.
Check the book "Anatomy of LISP", which is a classic. All the code in the book is written in M-Expressions. Now, Racket gets a new syntax (-> Rhombus). But the language core mechanisms are still there. I'm a fan of the s-expression syntax, but I've also heard&read from many people (incl. McCarthy), that THEY preferred a different syntax. I've seen Apple defining Dylan (Dynamic Language) with an s-expression syntax and then changing the language to a different syntax for broader adoption. You (and me) think it is essential. McCarthy and others thought that it was the wrong syntax for getting wider adoption.
Btw., personally, I don't care that much what Rich Hickey says. I can think for myself.
In the TXR Lisp dialect, classic functions like mapcar iterate over nonlists, without losing a shred of backward compatibility over lists.
For instance, we can iterate starting from an integer, in parallel with iterating over a list:
1> (mapcar 'cons '(a b c) 0)
((a . 0) (b . 1) (c . 2))
What we do is pattern the iteration abstraction after the car/cdr model, so that car/cdr iteration falls out as a straightforward special case.To do this we define an iteration API with four operations:
iter-begin: construct the iterator from the sequence object
iter-more: test the iterator whether it has more elements
iter-item: if iter-more tested true, get the first item
iter-step: calculate iterator of rest of sequence
iter-step may be functional or destructive, so the caller must capture the new iterator returned as a value and must stop using the old one.Integer:
1> (iter-begin 0) ;; identity
0
2> (iter-more 0) ;; true function: ignores argument, returns t.
t
3> (iter-item 0) ;; identity
0
4> (iter-step 0) ;; successor function
1
String: 5> (iter-begin "abc") ;; opaque iterator returned
#<seq-iter: a0bef50>
6> (iter-more *5)
t
7> (iter-item *5)
#\a
8> (iter-step *5) ;; destructively stepped
#<seq-iter: a0bef50>
9> (iter-item *8) ;; let's refer to *8 anyway
#\b
List: 10> (iter-begin '(1 2 3)) ;; identity, like in integer case
(1 2 3)
11> (iter-more '(1 2 3)) ;; not-equal-to-nil test
t
12> (iter-item '(1 2 3)) ;; car
1
13> (iter-step '(1 2 3)) ;; cdr with check
(2 3)
14> (iter-step '(1 . 2)) ;; demo of check
** iter-step: 2 is not a cons
Without the check in (iter-step '(1 . 2)) we would get 2, and that would then iterate through 2, 3, 4, ...Actually, yes he was.
user=> (list? (cons 1 '(2 3)))
true Clojure 1.11.3
user=> (list? (cons 1 '(2 3)))
falseIf it's inconsistent between variants then I'm absolutely not going to code in it...
In Clojurescript, the implementation details and type checks differ slightly from Clojure due to the different runtime environments (Javascript vs. JVM). As a result, certain predicates like `list?` might behave differently.
In Clojure, it's generally better to use (seq?) instead of (list?) when you need to check if a value is a sequence, as (seq?) is more encompassing and works for all sequence types, not just lists.
You almost never use (list?) in Clojure unless you explicitly need to check for a list created via (list ...), and most of the time we use (seq?) instead.
Maybe Clojure doesn't work for you because you haven't even bothered checking the documentation? At least you could've asked Google or ChatGPT, before crying how "broken it is". The language after all wasn't created last weekend, it's been around for 17 years
Also, is there anything good about cons? It seems like the most obtuse way to do a sequence unless you need something funky that you could implement yourself.
Lisps prior to Common Lisp usually had elaborate ways to handle runtime errors and extensive REPL interfaces for it, for example the break loop -> a REPL in the error context. The most prominent and widely cited was the one of Interlisp, which featured also user facing automatic error repair (-> DWIM).
Whenever I see a Lisp, which doesn't stop at an error and then let me look at it, I feel sad.
GNU Emacs is an application (a text editor) -> debug-on-error is by default NIL. That makes sense in an application.
> that never became a thing everyone wants, I guess it's not as critical as you paint it to be.
People asked for it, it seems. But the answer was no.
See for example "https://vimeo.com/223309989", "REPL driven development". It was a topic in the Q&A at the end.
Also: Since JVM/Java already has a built-in exception handling, why build another one?
There are some libraries...
[1] https://lisp-docs.github.io/cl-language-reference/chap-9/j-b...
https://andreyor.st/posts/2021-04-17-condition-system-in-clo...
https://www.reddit.com/r/Clojure/comments/mocs6d/common_lisp...
Sounds like a problem that exists in every single programming language to date. And the reason that problem appears is not because of the languages themselves.
Of course. That's why some massive projects got abandoned because nobody understood the code they were originally written in. Does anyone even remember Emacs, or AutoCAD, or Datomic? There were companies that went bankrupt because their Lisp codebases became obsolete. Remember Cisco? Or Apple (those idiots tried to build their payment system in Clojure), or Walmart? Or NASA (before they succumbed to the will of evil Elon). Maybe smaller companies like Pitch, or Funding Circle, or CircleCI, or Grammarly? No? Sigh, nobody cares anymore about the history of dead programming languages.
We abandoned a project in Python. Is Python unusable?
If you are comparing it to the ones you are used to, of course you'll find it harder.
(IMO, I don't like the Lisp syntax, but it's really not "non-readable".)
How other programming languages limit the depth of this recursion and the variation/complexity of a code block, by splitting the semantics into a limited set of reusable syntactic constructs is very powerful. It is sad that most aren't homoiconic though.
What isn't? Java or C# with deep hierarchies of nested classes and tons of boilerplate, extensive use of interfaces or overuse of exception checking?
Python with its magic methods and decorators and misindentation?
Javascript with the callback hell, inconsistent type coersion and sometimes tricky async code?
Ruby with its weird metaprogramming and monkey patching?
C with the pointers mental acrobatics and pre-processor macros?
Perl with weird default vars like $_ and tons of regexps?
Haskell with its monadic structures and heavy use of weird symbols and operators?
PHP with the crapload of its historical baggage, global vars and inconsistent, non-standardized function naming?
Any programming language can be considered hard to read, quirky, slow, inconsistent, or lacking some feature. Everyone knows there are only three God-given, bullet-proof, guaranteed ways to write robust, fast, consistent, bug-free and scalable software. Three... Humankind has yet to discover those.
You can easily read Lisp code off your phone screen - it wraps around, but retains the structure. Try that trick with pretty much any non-lispy language - even "the most elegant Python" fails there, face flat.
EDN & Transit is far better than JSON - EDN is more flexible and expressive than JSON, making it a superior choice for data serialization. It's also more than two times smaller and cleaner, commas are completely optional.
Hiccup is better than JSX & HTML - far more easier, cleaner, shorter and nicer to work with.
similarly,
s-expressions are better that XML & Yaml;
CL's PLISTs and ALISTs & Clojure maps are better than Dicts/Hashes in other languages.
Basically, you can leverage the expressiveness and simplicity of Lisp-like syntax to offer structured, readable, and powerful data representations. Add to that the ability to evaluate any given form on the fly through a connected REPL, basically "trying things while you read them," and that changes everything you thought you knew about the readability of code.
Those who call Lisp "non-readable" I suppose, in fact, never tried to actually read it. Of course, if you never learn a language, it would remain incomprehensible.
Defensive? No. I don't think Lisp needs defending.
Lisp is an idea. Good ideas need no defending. The invention of Lisp is one of the greatest events in the recent history of symbolic communication. It can be put in the same category as cave drawings, cuneiform, Egyptian hieroglyphs, and mathematical notation in the sense that they all represent systems of symbolic communication.
My "Pfff..." really comes from not understanding why people would say things like "Lisp is hard to read" when, in truth, it's probably the best tool we humans have for its designated purpose. Lisp emphasizes simplicity and uniformity, which can enhance readability and ease of understanding programs. With a big caveat, though, Lisp's readability improves with practice and familiarity. Many people find Lisp structures to be clear and expressive for symbolic manipulation and functional programming tasks. Okay, I mean I get it. Let's say I can, for example, look inside "Principia Mathematica" and completely find it unreadable. Yeah, I would. But wouldn't it be weird of me to go to a forum for mathematics and tell them that their math is unreadable? They'd probably decide that I'm trolling.
There are a couple standard ways to do dictionaries (a-lists and p-lists). Object systems like CLOS and GOOPS have standard text representations, but they're tied to their respective Lisps.
But like I said: there's EDN - primarily used for configuration files, data serialization/deserialization between systems, inter-process communication
And there's Transit - Ideal for scenarios requiring fast data transport over networks, such as web services and APIs, it supports both JSON and MessagePack encodings.