~ / track A / clojure basics

nil, truthy values, and nil-punning

Basic

Clojure's truthiness rule is one of the simplest in any language: nil and false are falsy. Everything else — including 0, "", [], {}, #{} — is truthy. This single rule, combined with the way the core library handles nil, gives Clojure a distinctive style called nil-punning: write the happy path, let nil flow through, and short- circuit at the edges.

The rule

(if nil   :truthy :falsy)   ;; => :falsy
(if false :truthy :falsy)   ;; => :falsy
(if 0     :truthy :falsy)   ;; => :truthy
(if ""    :truthy :falsy)   ;; => :truthy
(if []    :truthy :falsy)   ;; => :truthy
(if {}    :truthy :falsy)   ;; => :truthy

If you're coming from Python or Ruby, the surprising one is (if [] :t :f) returning :truthy. Use seq to ask "is it empty?":

(if (seq xs) :has-items :empty)

The reason: an empty collection is still a collection. Emptiness is a property to ask about, not a default-falsiness behavior.

What returns nil and what doesn't

Many core operations return nil for "no result," which makes nil-punning work:

(get {:a 1} :missing)        ;; => nil
(first [])                   ;; => nil
(rest [])                    ;; => () — empty seq, NOT nil
(next [])                    ;; => nil — `next` is `nil`-aware where `rest` isn't
(some odd? [2 4 6])          ;; => nil
(re-find #"\d" "abc")        ;; => nil
({:a 1} :missing :fallback)  ;; => :fallback (maps take a default arg)

The next vs rest distinction trips everyone once. Rule of thumb: if you want a recursion guard that ends on the empty list, use next (returns nil) and check with when-let.

nil-punning idioms

Because (map f nil) → (), (filter p nil) → (), (count nil) → 0, (seq nil) → nil, much of the standard library accepts nil as "the empty thing":

(count nil)        ;; => 0
(empty? nil)       ;; => true
(map inc nil)      ;; => ()
(reduce + nil)     ;; => 0
(concat nil [1 2]) ;; => (1 2)

This means you can pass nil through pipelines without checking for it. Combined with some-> and some->>, you get short-circuiting "if anything returned nil, stop":

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

That's Clojure's Maybe monad written out in two characters.

nil vs false — when the distinction matters

For booleans, two operators behave differently:

(if x ...)             ;; treats nil and false the same
(if (some? x) ...)     ;; true iff x is NOT nil  (false counts as present)
(if (boolean x) ...)   ;; coerces to true/false

if-some and when-some exist precisely so you can write "if a value is present (not nil), even if it's false":

(if-some [v (get config :debug)]
  (use-debug v)        ;; runs even if v is false
  (default-debug))

A common bug: storing false as a legitimate config value, then losing it because the code uses if-let instead of if-some.

and, or, nil, and short-circuiting

and and or return values, not booleans — they short-circuit and return the deciding value:

(or  nil false 7 10)   ;; => 7   ;; first truthy
(and 1 2 3 nil 5)      ;; => nil ;; first falsy
(or  x default)        ;; idiom: "x if present, otherwise default"
(and x (.something x)) ;; idiom: "call .something only if x non-nil"

The (or x default) idiom is everywhere; it's the analog of ?? in JavaScript/Swift, except it falls through on false too — which is sometimes a bug source, again.

Avoiding NPEs from Java interop

Java doesn't share the nil discipline. Calling .length on nil throws NPE. Wrap with some-> or check:

(some-> s .length)            ;; => nil if s is nil
(when s (.length s))          ;; equivalent

Real-world

PatternWhere it shows up
some-> for nested-map navigationReading config, parsing API responses, traversing parsed JSON
(or x default) for optional valuesArgument defaults, env-var reading, config merging
seq to test "non-empty"Loop termination, "should I render this section?"
if-some for boolean-valued optionsFeature flags, debug toggles
nil-tolerant pipelines (map, filter, concat)Aggregating partial results from concurrent calls
Datomic / Datalog: missing attributes return nilPull queries silently omit absent attrs
Reitit / Ring: middleware returns nil to skipAuth middleware that returns the request unchanged is common

Check yourself

? quiz

What does `(if [] :truthy :falsy)` return in Clojure, and why?

Exercise

Given the nested map:

(def order
  {:id 42
   :customer {:email "a@b.com" :address nil}
   :items [{:sku "X1" :qty 3}]})

Use some-> to write a function order-city that returns the customer's address city, returning nil cleanly if any link in the chain is missing. Then write the same function using only if-let and let. Compare the character counts and the readability.

 status: new