(Lisp)
The language that thinks in parentheses. A gentle, hands-on introduction to one of the oldest and most powerful programming languages ever created.
01 What is Lisp?
Lisp (short for List Processing) is one of the oldest high-level programming languages still in widespread use today. Invented in 1958 by John McCarthy at MIT, it pioneered ideas that are now standard in modern languages — garbage collection, dynamic typing, first-class functions, and the read-eval-print loop (REPL).
At its core, Lisp is built around a single, elegant idea: code and data share the same structure. Programs are written as nested lists, and those lists can themselves be treated as data, manipulated, and generated at runtime. This property — called homoiconicity — makes Lisp uniquely powerful for metaprogramming and building domain-specific languages.
Homoiconic
Code is data. Programs are just lists you can inspect and modify at runtime.
Functional
First-class functions, closures, and higher-order programming baked in from day one.
Macros
Extend the language itself. Write code that writes code — true metaprogramming.
Dynamic
Interactive development via the REPL — evaluate expressions and see results instantly.
02 A Brief History
John McCarthy conceived Lisp in 1958 while working on artificial intelligence research at MIT. He needed a language powerful enough to reason about symbolic expressions — things like math formulas, game trees, and logical propositions. The result was a language of startling simplicity built on just a handful of core primitives.
In 1960, McCarthy published "Recursive Functions of Symbolic Expressions and Their Computation by Machine", one of the most influential papers in computer science history. It defined the theoretical foundations of Lisp using lambda calculus.
| Year | Milestone |
|---|---|
| 1958 | John McCarthy designs Lisp at MIT |
| 1960 | Landmark paper establishes Lisp's mathematical foundations |
| 1962 | First Lisp compiler written (by Tim Hart and Mike Levin) |
| 1975 | Scheme published — a minimal, elegant Lisp dialect |
| 1984 | Common Lisp standardization effort begins |
| 1994 | ANSI standard for Common Lisp finalized |
| 2007 | Clojure introduced — Lisp on the JVM |
| Present | Lisp dialects thrive in AI, finance, and scripting |
03 Dialects
Lisp is not a single language — it's a family. Each dialect shares the fundamental parenthesized syntax and list-centric worldview, but they diverge in their standard libraries, type systems, and target platforms.
Common Lisp
The most feature-rich dialect. ANSI-standardized, with a huge standard library covering everything from CLOS (object-oriented programming) to networking. Best for large applications. Recommended for beginners wanting depth.
Scheme
A small, clean, rigorously defined dialect. Beloved in academia, used to teach CS fundamentals (MIT's classic textbook SICP uses Scheme). Tail-call optimization is mandatory in the standard.
Clojure
A modern Lisp that runs on the Java Virtual Machine. Emphasizes immutability and designed for concurrent programming. Interoperates fully with Java libraries. Very popular in the enterprise world.
Emacs Lisp
The dialect powering the GNU Emacs text editor. Writing Emacs Lisp lets you customize and extend one of the most powerful editors in existence. A great practical entry point.
04 Getting Started
The fastest way to run Common Lisp is to install SBCL (Steel Bank Common Lisp), a free, high-performance implementation.
Install SBCL
# macOS (Homebrew)
brew install sbcl
# Ubuntu / Debian
sudo apt install sbcl
# Windows — download the installer from sbcl.org
# or use WSL with the Linux instructions above
# Start the interactive REPL
sbcl
Your First Expression
Once the REPL is running (you'll see a * prompt), type your first Lisp expression:
05 Syntax & S-Expressions
Lisp's syntax is built on a single structure: the S-expression (symbolic expression). An S-expression is either an atom (a number, symbol, or string) or a list enclosed in parentheses.
Every list is interpreted as a function call: the first element is the function (or operator), and the remaining elements are the arguments. This is called prefix notation.
; Form: (function arg1 arg2 ...)
(+ 3 4) ; → 7 (addition)
(- 10 3) ; → 7 (subtraction)
(* 6 7) ; → 42 (multiplication)
(/ 10 2) ; → 5 (division)
; Nesting: innermost evaluates first
(+ (* 2 3) (- 10 4)) ; → 12 (2×3=6, 10-4=6, 6+6=12)
; Arithmetic on many numbers at once
(+ 1 2 3 4 5) ; → 15
(max 3 7 2 9) ; → 9
; on the same line is a comment and is ignored by the interpreter. Use ;; for section-level comments — it's a common convention.Quoting — Preventing Evaluation
By default, Lisp evaluates every list as a function call. To treat a list as raw data instead, quote it with the ' shorthand (or the quote special form):
(+ 1 2) ; Evaluates → 3
'(+ 1 2) ; Quoted → (+ 1 2) (the list itself)
'hello ; A quoted symbol → HELLO
'(cat dog bird) ; A list of symbols
; Long form — identical result:
(quote (a b c)) ; → (A B C)
06 Data Types
Common Lisp has a rich, dynamic type system. Types are checked at runtime, and variables can hold any type.
| Type | Example | Notes |
|---|---|---|
| Integer | 42, -7, 0 | Arbitrary precision — no overflow |
| Float | 3.14, -0.5 | IEEE 754 double precision |
| Ratio | 1/3, 22/7 | Exact fractions — no rounding |
| String | "Hello" | Double-quoted, mutable character arrays |
| Character | #\A, #\space | Single characters prefixed with #\ |
| Symbol | FOO, my-var | Named identifiers, case-insensitive by default |
| List | (1 2 3), (a b c) | Singly-linked lists — the heart of Lisp |
| Boolean | T, NIL | NIL is false AND the empty list; everything else is true |
| Vector | #(1 2 3) | Fixed-size, O(1) random-access arrays |
| Hash Table | make-hash-table | Key-value store with O(1) average access |
NIL and the empty list () are the same object. This means an empty list is always falsy — a useful and intentional design choice.Type Predicates
(integerp 42) ; → T
(stringp "hi") ; → T
(listp '(1 2)) ; → T
(null nil) ; → T (NIL is the empty list)
(numberp "x") ; → NIL
; typep for general type checking
(typep 3.14 'float) ; → T
(type-of "hello") ; → (SIMPLE-ARRAY CHARACTER (5))
07 Variables
Lisp has two kinds of variables: global (dynamic) variables defined with defvar or defparameter, and local variables bound with let.
;; defvar — only sets the value if unbound
(defvar *player-name* "Alice")
;; defparameter — always resets the value
(defparameter *max-lives* 3)
(defparameter *pi-approx* 3.14159)
;; Mutate with setf
(setf *max-lives* 5) ; *max-lives* is now 5
;; Constants
(defconstant +speed-of-light+ 299792458)
*like-this*. Constants use +plus-signs+. These are naming conventions, not syntax rules, but following them makes your code immediately readable to other Lispers.Local Variables with let
;; let — bindings are evaluated in parallel
(let ((x 10)
(y 20))
(+ x y)) ; → 30
;; let* — bindings evaluated sequentially (each can see previous)
(let* ((base 5)
(area (* base base)))
(format t "Area: ~a~%" area))
; Prints: Area: 25
08 Functions
Functions are defined with defun. They are first-class values in Lisp — they can be passed as arguments, returned from other functions, and stored in variables.
;; Basic function definition
(defun greet (name)
(format t "Hello, ~a!~%" name))
(greet "World") ; Prints: Hello, World!
;; Returning a value (last expression is the return value)
(defun square (n)
(* n n))
(square 7) ; → 49
;; Multiple parameters
(defun hypotenuse (a b)
(sqrt (+ (square a)
(square b))))
(hypotenuse 3 4) ; → 5.0
Optional & Keyword Arguments
;; &optional — has a default value if not supplied
(defun power (base &optional (exp 2))
(expt base exp))
(power 3) ; → 9 (3²)
(power 2 10) ; → 1024 (2¹⁰)
;; &key — named arguments (order doesn't matter)
(defun describe-person (&key name (age 0))
(format t "~a is ~a years old.~%" name age))
(describe-person :name "Bob" :age 30)
; → Bob is 30 years old.
Anonymous Functions (Lambda)
;; Create a function without naming it
(lambda (x) (* x x))
;; Call it immediately
((lambda (x) (* x x)) 5) ; → 25
;; Store it in a variable with #' (function operator)
(let ((double (lambda (x) (* 2 x))))
(funcall double 7)) ; → 14
09 Conditionals
Lisp provides several conditional forms. The most fundamental is if, but cond, when, and unless cover common patterns more expressively.
;; if: (if test then-expr else-expr)
(if (> 5 3)
"five is greater"
"three is greater")
; → "five is greater"
;; when: runs only if test is true; can have multiple body forms
(when (> *max-lives* 0)
(format t "Player is alive!~%"))
;; unless: runs only if test is NIL (false)
(unless (zerop *max-lives*)
(decf *max-lives*)) ; decrement by 1
;; cond: like an if/else-if chain
(defun classify-temp (c)
(cond
((< c 0) "freezing")
((< c 15) "cold")
((< c 25) "comfortable")
(t "hot"))) ; t = default (else)
(classify-temp 22) ; → "comfortable"
Comparison Operators
| Operator | Meaning | Example | Result |
|---|---|---|---|
| = | Numeric equality | (= 3 3) | T |
| /= | Numeric inequality | (/= 3 4) | T |
| eq | Object identity | (eq 'a 'a) | T |
| eql | Numbers or identity | (eql 42 42) | T |
| equal | Structural equality | (equal '(1 2) '(1 2)) | T |
| string= | String equality | (string= "hi" "hi") | T |
10 Loops
Common Lisp's loop macro is extraordinarily powerful — almost a mini-language of its own. For simpler cases, dotimes and dolist are more readable.
;; dotimes — repeat N times (i goes 0 to N-1)
(dotimes (i 5)
(format t "~a " i))
; Prints: 0 1 2 3 4
;; dolist — iterate over a list
(dolist (fruit '(apple banana cherry))
(format t "I like ~a~%" fruit))
;; loop macro — powerful iteration
(loop for i from 1 to 5
collect (* i i))
; → (1 4 9 16 25) (list of squares)
;; loop with filtering
(loop for i from 1 to 20
when (evenp i)
collect i)
; → (2 4 6 8 10 12 14 16 18 20)
;; loop with accumulation
(loop for i from 1 to 100 sum i)
; → 5050 (sum of 1 to 100)
11 Recursion
Recursion is the idiomatic way to express repetition in Lisp. A recursive function calls itself with a smaller version of the problem until it reaches a base case.
;; Factorial: n! = n × (n-1)!
(defun factorial (n)
(if (<= n 1)
1
(* n (factorial (- n 1)))))
(factorial 10) ; → 3628800
;; Fibonacci sequence
(defun fib (n)
(cond
((= n 0) 0)
((= n 1) 1)
(t (+ (fib (- n 1))
(fib (- n 2))))))
(fib 10) ; → 55
;; Recursive list sum
(defun list-sum (lst)
(if (null lst)
0
(+ (car lst)
(list-sum (cdr lst)))))
(list-sum '(1 2 3 4 5)) ; → 15
loop for performance-critical iterations, or write explicitly tail-recursive code with a compiler that supports TCO like SBCL.12 List Operations
Lists are the native data structure of Lisp. They are built from cons cells — each cell holds a value (car) and a pointer to the rest of the list (cdr). Understanding cons cells unlocks all of Lisp.
(defparameter *nums* '(10 20 30 40))
(car *nums*) ; → 10 (first element)
(cdr *nums*) ; → (20 30 40) (rest of list)
(cadr *nums*) ; → 20 (second — car of cdr)
(caddr *nums*) ; → 30 (third)
(last *nums*) ; → (40) (last cons cell)
;; Building lists
(cons 5 '(6 7)) ; → (5 6 7) (prepend)
(list 1 2 3) ; → (1 2 3)
(append '(1 2) '(3 4)) ; → (1 2 3 4)
;; Inspection
(length *nums*) ; → 4
(reverse *nums*) ; → (40 30 20 10)
(member 20 *nums*) ; → (20 30 40) — tail from match
(nth 2 *nums*) ; → 30 (0-indexed)
(sort '(3 1 4 1 5) #'<) ; → (1 1 3 4 5)
13 Higher-Order Functions
Higher-order functions take other functions as arguments or return them. They let you express patterns like mapping, filtering, and folding without writing explicit loops.
;; mapcar — apply a function to every element
(mapcar #'square '(1 2 3 4))
; → (1 4 9 16)
(mapcar (lambda (x) (* x 10)) '(1 2 3))
; → (10 20 30)
;; remove-if — filter out matching elements
(remove-if #'evenp '(1 2 3 4 5 6))
; → (1 3 5) (removed even numbers)
(remove-if-not #'evenp '(1 2 3 4 5 6))
; → (2 4 6) (keep only evens)
;; reduce — fold a list into a single value
(reduce #'+ '(1 2 3 4 5)) ; → 15
(reduce #'* '(1 2 3 4 5)) ; → 120
(reduce #'max '(3 9 1 7)) ; → 9
;; Combining them — pipeline style
(reduce #'+
(mapcar #'square
(remove-if #'evenp '(1 2 3 4 5))))
; Sum of squares of odd numbers: 1+9+25 = 35
14 Macros
Macros are Lisp's most powerful feature. Unlike functions, macros receive their arguments unevaluated — as raw syntax — and return new code that Lisp then evaluates. This lets you extend the language with new control structures, DSLs, and compile-time transformations.
;; A simple macro: swap two variables
(defmacro swap! (a b)
`(let ((tmp ,a))
(setf ,a ,b)
(setf ,b tmp)))
(let ((x 1) (y 2))
(swap! x y)
(list x y)) ; → (2 1)
;; A macro: my-when (like built-in when)
(defmacro my-when (test &body body)
`(if ,test
(progn ,@body)))
;; Check what code a macro expands to:
(macroexpand-1 '(my-when t (print "hi")))
; → (IF T (PROGN (PRINT "hi")))
` is a quasiquote — like a regular quote, but commas , selectively evaluate expressions inside it. ,@ splices a list in. These are the building blocks of macro templates.15 Practical Examples
Let's put everything together with a few short but realistic programs.
FizzBuzz
(defun fizzbuzz (n)
(loop for i from 1 to n do
(format t "~a~%"
(cond
((zerop (mod i 15)) "FizzBuzz")
((zerop (mod i 3)) "Fizz")
((zerop (mod i 5)) "Buzz")
(t i)))))
(fizzbuzz 15)
Flatten a Nested List
(defun flatten (lst)
(cond
((null lst) nil)
((listp (car lst))
(append (flatten (car lst))
(flatten (cdr lst))))
(t
(cons (car lst)
(flatten (cdr lst))))))
(flatten '(1 (2 (3 4)) (5 (6 7 (8)))))
; → (1 2 3 4 5 6 7 8)
Simple Word Counter
(defun word-frequencies (words)
(let ((freq (make-hash-table :test #'equal)))
(dolist (word words)
(incf (gethash word freq 0)))
freq))
(let ((counts (word-frequencies
'(the cat sat on the mat the cat))))
(format t "the: ~a~%" (gethash 'the counts))
(format t "cat: ~a~%" (gethash 'cat counts)))
; the: 3
; cat: 2