Beginner

Control Flow in Common Lisp

Master control flow in Common Lisp - conditionals with if/when/unless/cond/case, iteration with the loop macro, and recursion, all with Docker-ready examples

Control flow is how a program decides what to do next: which branch to take, how many times to repeat work, and when to stop. Most languages express this with statements like if, for, and while. Common Lisp is different in a crucial way — control flow constructs are expressions that return values, and they are built from the same parenthesized S-expressions as everything else in the language.

As a multi-paradigm language, Common Lisp gives you a remarkably rich toolkit. There are several conditional forms (if, when, unless, cond, case), several iteration constructs (dotimes, dolist, do, and the famously powerful loop macro), and — because Lisp grew out of the functional tradition — recursion as a first-class way to express repetition. Rather than one canonical loop, you choose the form that best fits the shape of the problem.

In this tutorial you’ll learn how to branch with the conditional forms, iterate with the looping macros, control loops with early exits and skipping, and express repetition functionally with recursion. Each example is a complete program you can run with SBCL under Docker.

Conditionals

Common Lisp’s conditional forms range from the basic two-way if to the multi-way cond and value-dispatching case. Because these are expressions, if doubles as the language’s “ternary” operator — it simply returns the value of whichever branch runs.

Create a file named conditionals.lisp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
;; if - takes a test, a then-form, and an optional else-form
(let ((x 10))
  (if (> x 0)
      (format t "~a is positive~%" x)
      (format t "~a is not positive~%" x)))

;; if is an expression - it returns a value (acts like a ternary)
(let* ((n 7)
       (parity (if (evenp n) "even" "odd")))
  (format t "~a is ~a~%" n parity))

;; when - run the body only when the test is true (no else branch)
(let ((temp 95))
  (when (> temp 90)
    (format t "It's hot! Temperature is ~a~%" temp)))

;; unless - run the body only when the test is false
(let ((logged-in nil))
  (unless logged-in
    (format t "Please log in~%")))

;; cond - multi-way branching, like if/else-if/else
(let ((score 85))
  (cond ((>= score 90) (format t "Grade: A~%"))
        ((>= score 80) (format t "Grade: B~%"))
        ((>= score 70) (format t "Grade: C~%"))
        (t             (format t "Grade: F~%"))))

;; case - dispatch on a single value, like switch
(let ((day 3))
  (case day
    (1 (format t "Monday~%"))
    (2 (format t "Tuesday~%"))
    (3 (format t "Wednesday~%"))
    (otherwise (format t "Another day~%"))))

A few things to notice. if accepts exactly one then-form and one optional else-form — when you need multiple statements in a branch, reach for when/unless (which wrap their body in an implicit progn) or cond. In cond, each clause is (test form...) and the first clause whose test is true wins; the final t clause acts as the catch-all “else”. case compares its key against the literal values in each clause and uses otherwise (or t) as the default.

Iteration with Loops

Common Lisp offers several looping constructs. dotimes repeats a fixed number of times, dolist walks the elements of a list, do is the general-purpose loop with explicit stepping, and the loop macro is a miniature language of its own for accumulating and iterating.

Create a file named loops.lisp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
;; dotimes - iterate from 0 up to n-1
(format t "Counting with dotimes:~%")
(dotimes (i 5)
  (format t "  i = ~a~%" i))

;; dolist - iterate over the elements of a list
(format t "Iterating with dolist:~%")
(dolist (fruit '("apple" "banana" "cherry"))
  (format t "  ~a~%" fruit))

;; loop - accumulate a sum
(format t "Summing with loop:~%")
(let ((total (loop for n from 1 to 5 sum n)))
  (format t "  Sum 1..5 = ~a~%" total))

;; loop - collect results into a new list
(let ((squares (loop for n from 1 to 5 collect (* n n))))
  (format t "  Squares: ~a~%" squares))

;; do - general iteration with an explicit step and stop condition
(format t "Countdown with do:~%")
(do ((i 3 (- i 1)))   ; bind i = 3, step by subtracting 1 each pass
    ((zerop i))        ; stop when i reaches 0
  (format t "  ~a~%" i))

The loop macro is worth dwelling on: loop for n from 1 to 5 sum n reads almost like English and replaces the manual accumulator-and-counter boilerplate found in most languages. The collect clause gathers values into a fresh list. do is more verbose but fully general — each variable spec is (var init step), and the loop stops as soon as the termination test becomes true.

Loop Control: Early Exit and Skipping

Sometimes you need to break out of a loop early or skip an iteration. Common Lisp uses return (and return-from) to exit a loop immediately. There is no dedicated continue keyword — the idiomatic approach is to guard the body with when/unless so unwanted iterations simply do nothing.

Create a file named loop_control.lisp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
;; Early exit with return - stop at the first multiple of 7
(format t "Searching for first multiple of 7:~%")
(dotimes (n 50)
  (when (and (> n 0) (zerop (mod n 7)))
    (format t "  Found: ~a~%" n)
    (return)))            ; leave the dotimes immediately

;; A bare loop runs forever until you return out of it
(format t "Loop until a condition:~%")
(let ((i 0))
  (loop
    (when (>= i 3) (return))
    (format t "  i = ~a~%" i)
    (incf i)))            ; incf increments i in place

;; "continue"-style skipping: guard the body instead of jumping
(format t "Odd numbers only:~%")
(dotimes (n 6)
  (unless (evenp n)
    (format t "  ~a~%" n)))

The first loop scans 0 through 49 and bails out with (return) the moment it finds 7. The second shows the simplest infinite loop — it has no clauses, so it repeats its body until something returns. The third demonstrates the Lisp way to “skip” an iteration: rather than a continue jump, you wrap the work in unless so even numbers fall through with nothing done.

Recursion: Functional Control Flow

Because Common Lisp descends from the functional tradition, recursion is a natural and idiomatic way to express repetition — especially when walking recursive data like lists. Instead of mutating a counter, a recursive function calls itself with a smaller problem until it reaches a base case.

Create a file named recursion.lisp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
;; Classic recursion: factorial
(defun factorial (n)
  (if (<= n 1)
      1                              ; base case
      (* n (factorial (- n 1)))))    ; recursive case

;; Recursion as a loop: count down and print each value
(defun countdown (n)
  (when (> n 0)
    (format t "~a " n)
    (countdown (- n 1))))

;; Recursion over a list using first/rest
(defun sum-list (lst)
  (if (null lst)
      0                                       ; empty list sums to 0
      (+ (first lst) (sum-list (rest lst))))) ; head + sum of tail

(format t "5! = ~a~%" (factorial 5))
(format t "Countdown: ")
(countdown 5)
(format t "~%")
(format t "Sum of (1 2 3 4 5) = ~a~%" (sum-list '(1 2 3 4 5)))

Each function has a base case that stops the recursion and a recursive case that reduces the problem. factorial shrinks n toward 1; sum-list peels off the first element and recurses on the rest until the list is empty. This pattern — base case plus a step toward it — is the recursive equivalent of a loop’s termination test and update step.

Running with Docker

Each example is a standalone script you can run with SBCL inside Docker, with no local installation required.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the SBCL image (Steel Bank Common Lisp)
docker pull clfoundation/sbcl:latest

# Run the conditionals example
docker run --rm -v $(pwd):/app -w /app clfoundation/sbcl:latest sbcl --script conditionals.lisp

# Run the loops example
docker run --rm -v $(pwd):/app -w /app clfoundation/sbcl:latest sbcl --script loops.lisp

# Run the loop control example
docker run --rm -v $(pwd):/app -w /app clfoundation/sbcl:latest sbcl --script loop_control.lisp

# Run the recursion example
docker run --rm -v $(pwd):/app -w /app clfoundation/sbcl:latest sbcl --script recursion.lisp

Expected Output

Running conditionals.lisp:

10 is positive
7 is odd
It's hot! Temperature is 95
Please log in
Grade: B
Wednesday

Running loops.lisp:

Counting with dotimes:
  i = 0
  i = 1
  i = 2
  i = 3
  i = 4
Iterating with dolist:
  apple
  banana
  cherry
Summing with loop:
  Sum 1..5 = 15
  Squares: (1 4 9 16 25)
Countdown with do:
  3
  2
  1

Running loop_control.lisp:

Searching for first multiple of 7:
  Found: 7
Loop until a condition:
  i = 0
  i = 1
  i = 2
Odd numbers only:
  1
  3
  5

Running recursion.lisp:

5! = 120
Countdown: 5 4 3 2 1 
Sum of (1 2 3 4 5) = 15

Key Concepts

  • Conditionals are expressionsif, cond, and case all return values, so if doubles as Common Lisp’s ternary operator.
  • Pick the right conditional — use if for a single two-way choice, when/unless for a one-sided branch with multiple body forms, cond for multi-way tests, and case to dispatch on a single value.
  • t and otherwise are the catch-alls — the final t clause in cond and otherwise in case act as the “else” branch.
  • Many loops, one for each jobdotimes for fixed counts, dolist for list elements, do for general stepping, and loop for expressive accumulation with clauses like sum and collect.
  • return exits a loop early — there is no continue keyword; skip an iteration by guarding the body with when/unless instead.
  • Recursion replaces iteration — with a base case and a step toward it, recursive functions express repetition naturally, especially when processing lists with first and rest.
  • nil is false, everything else is true — only nil (the empty list) counts as false in a test, which makes list-walking base cases like (null lst) clean and idiomatic.

Running Today

All examples can be run using Docker:

docker pull clfoundation/sbcl:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining