~ / track A / clojure basics

Flow control: if, when, cond, case

Basic

Most Clojure code is small expressions threaded through a handful of conditional and branching forms. There is no statement vs expression distinction — everything returns a value, even if — so flow control composes cleanly inside other expressions.

The core forms

FormReturnsWhen to reach for it
(if test then else)then or elseOne-shot branch with both arms
(when test body...)body or nilBranch that only matters when test is truthy
(when-not test body...)body or nilSame, inverted
(cond pred₁ v₁ pred₂ v₂ ...)First matching vᵢMulti-way branch on arbitrary predicates
(condp pred expr a r₁ b r₂ ...)rᵢ where (pred expr xᵢ) truthyMulti-way against one comparison
(case expr v₁ r₁ v₂ r₂ ... default)rᵢ for matching constantCompile-time hash-dispatched switch
(if-let [x expr] then else)binds x if truthy, then then; else else"Get the thing — if it exists, use it"
(when-let [x expr] body...)binds x if truthy, then body; else nilSame as above without an else branch
(if-some [x expr] ...)Like if-let, but only nil is falseDistinguishes false from missing
(when-some [x expr] ...)SameSame

Why so many?

Each form removes a tiny piece of noise that adds up. Compare:

;; if/let pyramid
(let [u (find-user id)]
  (if u
    (touch u)
    nil))

;; with if-let
(if-let [u (find-user id)]
  (touch u)
  nil)

;; with when-let (drop the redundant nil branch)
(when-let [u (find-user id)]
  (touch u))

That last form is what idiomatic Clojure actually looks like.

cond vs case vs condp

;; cond: arbitrary predicates, top-to-bottom
(cond
  (zero? n)     :zero
  (neg? n)      :negative
  (< n 100)     :small
  :else         :large)

;; case: equality against compile-time constants (hash-dispatched, fast)
(case status
  :ok      "200 OK"
  :missing "404 Not Found"
  :error   "500 Error"
  "unknown")          ;; trailing default

;; condp: one shared predicate against many right-hand values
(condp re-matches s
  #"\d+"       :digits
  #"[a-z]+"    :word
  :other)`

Common bug: case does not evaluate its left-hand values — they must be literal constants. (case x my-keyword …) will not work the way you expect; use cond or condp = instead.

do and side effects

if, when, let, fn each take a single expression as a body — but when and let and fn implicitly wrap the body in do, so you can write multiple forms. if does not:

(if cond
  (do                  ;; explicit do needed
    (println "going!")
    (run))
  (println "skipping"))

when and let are syntax sugar for (if test (do body) nil) and (let [...] (do body)) — a tiny rule that explains a lot.

for is not a loop

for is a list comprehension — it returns a lazy seq, it does not run for side effects. If you want to iterate for effect, use doseq:

(for [x (range 3) y (range 3)] [x y])    ;; produces a seq of pairs
(doseq [x (range 3)] (println x))         ;; prints, returns nil

For loops in the imperative sense, see loop/recur in [[recursion-and-recur]].

Try it in the REPL

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

Real-world

PatternWhere
cond for state-machine transitionsCompojure/Reitit handlers, finite-state-machine libraries
case for hot-path dispatchToken parsing, opcode interpreters, performance-sensitive code
condp for "which regex matches"Lexers, URL routers, content-type negotiation
if-let / when-let everywhereIdiomatic "get-or-skip" patterns in CRUD code
if-some / when-someWhen the value false is meaningful (not just "missing")
for for cartesian products / pipelinesGenerating test fixtures, building combinations
doseq for side-effecting iterationDatabase writes, logging, file output

Check yourself

? quiz

Which form should you reach for when you have one expression to compare against many literal constants in a tight loop?

Exercise

Write three versions of an HTTP status classifier that maps 200 → :ok, 3xx → :redirect, 4xx → :client-error, 5xx → :server-error, anything else → :unknown:

  1. Using cond.
  2. Using condp with <=.
  3. Using case for the canonical codes (200, 301, 400, 500), with a fallback to one of the above for the rest.

Which version do you actually want to read in six months?

 status: new