Navigating the Lisp Landscape: A Comparative Analysis of Common Lisp, Racket, Clojure, and Emacs Lisp
The Lisp family of languages is often viewed as a monolith of parentheses, yet beneath the surface lies a diverse ecosystem of dialects, each tailored for specific environments—from the extensible core of Emacs to the JVM-based concurrency of Clojure. For developers moving between these languages, the challenge is rarely the core logic, but rather the subtle shifts in syntax, standard libraries, and execution models.
This analysis synthesizes a comprehensive side-by-side reference of Common Lisp, Racket, Clojure, and Emacs Lisp, highlighting where they converge and where they diverge in their implementation of the Lisp philosophy.
Execution Models and Tooling
While all four dialects provide a Read-Eval-Print Loop (REPL), their paths to production vary significantly.
- Common Lisp (SBCL): Heavily focused on compilation to machine code. As noted by community contributors, SBCL often compiles code by default, even within the REPL, blurring the line between interpretation and compilation.
- Racket: Offers a sophisticated module system and the
racotoolset for compilation and package management. It is unique in its ability to easily produce standalone executables via#langdeclarations. - Clojure: Deeply integrated with the Java Virtual Machine (JVM). Its execution is tied to the Java ecosystem, utilizing
.jarfiles and the JVM's memory management and threading models. - Emacs Lisp: Primarily designed as an extension language for the Emacs editor. While it supports byte-compilation for performance, its primary mode of existence is within the editor's own process.
Core Syntactic Divergences
Truth and Falsehood
One of the most jarring differences for newcomers is the definition of "truthy" and "falsy" values. In Common Lisp and Emacs Lisp, nil and the empty list () are synonymous and evaluate to false. Racket follows a stricter path where only #f (or false) is false; null and () are considered true. Clojure occupies a middle ground, where false and nil are both falsy, but the empty list () is truthy.
Variable Scoping and Assignment
Variable declaration follows a general pattern of let for local and def/define for global variables, but the nuances matter:
- Lisp-1 vs Lisp-2: Common Lisp and Emacs Lisp are "Lisp-2s," meaning they maintain separate namespaces for functions and variables. A symbol can resolve to both a value and a function simultaneously. Clojure and Racket are "Lisp-1s," where a symbol refers to a single entity in a given environment.
- Dynamic vs Lexical Scope: Emacs Lisp historically relied on dynamic scope. While
lexical-letwas introduced to provide lexical scoping, the default behavior in older versions often differs from the strict lexical scoping found in Clojure and Racket.
Data Structure Nuances
The Evolution of the List
While the cons cell is the ancestral heart of Lisp, modern dialects have evolved:
- Clojure's Persistent Data Structures: Unlike the linked lists of Common Lisp or Racket, Clojure utilizes persistent, immutable data structures. This makes
consbehave differently; the second argument must be a list, and the original list remains unmodified. - The
car/cdrLegacy: Most dialects still supportcar(first) andcdr(rest), though there is a strong trend toward the more readablefirstandrestnomenclature. Clojure further simplifies this withfirstandnext(wherenextreturnsnilfor singleton lists, unlikerestwhich returns an empty sequence).
Arrays and Dictionaries
Fixed-length arrays (vectors) are ubiquitous, but their mutability varies. Racket distinguishes between immutable vectors (#()) and mutable ones created via (vector ...). Clojure leans heavily into immutable maps and vectors, providing a consistent API across different sequence types.
Advanced Language Features
Macros and Hygiene
Macros are the "killer feature" of Lisp, allowing the language to be extended. However, the approach to safety differs:
- Hygienic Macros: Racket is the gold standard for hygienic macros, ensuring that macro expansions do not accidentally capture variables from the surrounding scope.
- Non-Hygienic Macros: Common Lisp and Emacs Lisp macros are non-hygienic. To avoid name collisions, developers must manually generate unique symbols using
gensym.
Exception Handling and Restarts
Common Lisp provides a powerful "Condition System" that separates the detection of an error from its resolution. The restart-case allows a programmer to define multiple ways to recover from an error, which can be invoked by a handler without unwinding the stack—a feature largely absent from the other three dialects.
Summary Comparison Table
| Feature | Common Lisp | Racket | Clojure | Emacs Lisp |
|---|---|---|---|---|
| Namespace | Lisp-2 | Lisp-1 | Lisp-1 | Lisp-2 |
| Falsy Values | nil, () |
#f |
false, nil |
nil, () |
| Macros | Non-hygienic | Hygienic | Semi-hygienic | Non-hygienic |
| Primary Target | Native/Machine | Native/Bytecode | JVM | Emacs Editor |
| Default Scope | Lexical | Lexical | Lexical | Dynamic/Lexical |
Final Insights
The fragmentation of the Lisp family is often viewed as a hindrance to adoption, but as one community member noted, the experience of writing Lisp can feel like "reading poetry" due to its beautiful, flowing concepts. Whether you need the industrial-strength compilation of Common Lisp, the academic rigor of Racket, the JVM interoperability of Clojure, or the editor-integration of Emacs Lisp, the core philosophy remains the same: code is data, and the language is a canvas for the programmer.