Mastering Clojure Iteration: A Practical Guide
When to use for, map, doseq, and run!

I'm a web-focused developer with a passion for exploring new ideas and hopefully sharing more of them through this blog 😃
You're working with a collection in Clojure and need to iterate over it. You reach for... wait, which one?
formapdoseqrun!
In my experience, while the official documentation exists, it's often unclear when to use each construct. This guide breaks down the differences with practical examples, so you can make the right choice every time.
TLDR: Always use run! and doseq for side-effects, and map and for for lazy data transformations
1. map - Transform Collections
Purpose: Transform each element of a collection into something else.
Returns: A lazy sequence of transformed values.
When to use:
You want to convert one collection into another
You're applying a transformation function
You need the result for further processing
Characteristics:
Lazy - only computes values when needed
Returns a sequence - the transformed collection
Pure - no side effects expected
;; Transform numbers to their squares
(map #(* % %) [1 2 3 4])
;; => (1 4 9 16)
;; Extract user IDs
(map :user-id users)
;; => ("alice" "bob" "charlie")
;; Transform with a function
(map str/upper-case ["hello" "world"])
;; => ("HELLO" "WORLD")
When NOT to use map:
The key insight is that map is lazy, so side effects inside map can lead to surprising results.
If you're doing side effects, you probably want doseq or run! instead.
;; This creates a lazy sequence but doesn't execute
(def squares (map #(do (println "Computing" %)
(* % %))
[1 2 3]))
;; => No output yet!
;; Realizing the sequence triggers computation
(doall squares)
;; Computing 1
;; Computing 2
;; Computing 3
;; => (1 4 9)
2. for - List Comprehensions
Purpose: Build a collection with complex iteration logic (filters, multiple bindings, let bindings).
Returns: A lazy sequence.
When to use:
You need complex iteration with multiple sequences
You want to filter while iterating
You need
letbindings inside the iterationYou're translating a mathematical set notation
Characteristics:
Lazy - only computes when realized
Returns a sequence - the generated collection
Declarative - reads like "for each X in Y, produce Z"
;; Cartesian product with filter
(for [x [1 2 3]
y [4 5 6]
:when (even? (+ x y))]
[x y])
;; => ([1 5] [2 4] [2 6] [3 5])
;; Multiple bindings with let
(for [user users
:let [email (:email user)
domain (second (str/split email #"@"))]
:when (= domain "example.com")]
(:name user))
;; => ("Alice" "Bob")
;; Nested iteration
(for [feed feeds
item (:items feed)]
(process-item feed item))
;; Perfect for generating a deck of cards
(def suits #{:hearts :diamonds :clubs :spades})
(def ranks ["2" "3" "4" "5" "6" "7" "8" "9" "10" "J" "Q" "K" "A"])
(defn make-deck
"Create a 52-card deck"
[]
(shuffle
(for [suit suits
rank ranks]
{:suit suit :rank rank})))
Rule of thumb: Use for when you need :when, :let, or multiple sequences. Use map for simple one-to-one transformations.
Generally, follow the same rules as map Don’t use this when you need to generate side effects.
;; Unnecessarily verbose
(for [user users]
(:email user))
;; Better - Use map
(map :email users)
;; weird with side effects
=> (doall (map println [1 2 3]))
1
2
3
(nil nil nil)
3. doseq - Imperative Iteration for Side Effects
Purpose: Execute side effects for each element in a collection.
Returns: nil (you're not using the return value).
When to use:
You're doing I/O (printing, file writes, network calls)
You're mutating state (atoms, databases)
You need multiple operations per item
You need destructuring or complex bindings
Characteristics:
Eager - executes immediately
Returns nil - signals "I'm doing side effects"
Imperative - like a
forloop in other languages
;; doseq - Best for performing side effects for each result
;; Destructuring with complex logic with :keys
(doseq [{:keys [name email role]} users]
(when (= role "admin")
(send-admin-notification name email)))
;; Multiple operations per item
(doseq [order orders]
(send-confirmation-email order)
(update-inventory order)
(log-transaction order))
;; Nested iteration
(doseq [feed feeds]
(println "Processing" (:feed-id feed))
(doseq [item (:items feed)]
(save-to-db item)))
4. run! - Concise Side Effects
Purpose: Apply a single side-effecting function to each element.
Returns: nil.
When to use:
You're calling one side-effect producing function per item
You don't need destructuring
You want concise code
Characteristics:
Eager - executes immediately
Returns nil - signals side effects
Concise - one-liner for simple cases
run! is more concise for when you're just calling a single function
;; doseq - verbose
(doseq [alert alerts]
(println alert))
;; Better with run! - concise
(run! println alerts)
;; Send email to each user
(run! send-email users)
;; Save each document
(run! save-to-db documents)
;; Close each connection
(run! #(.close %) connections)
When You Shouldn’t Use run!
;; Multiple operations - better with doseq
(doseq [user users]
(send-email user)
(log-activity user)
(update-db user))
;; Destructuring
;; With run! you need a wrapper
(run! (fn [{:keys [name email]}]
(send-email name email))
users)
;; Better with doseq!
(doseq [{:keys [name email]} users]
(send-email name email))
;; Conditional logic
;; With run! you need the logic in the function
(run! (fn [user]
(when (active? user)
(send-notification user)))
users)
;; doseq is clearer
(doseq [user users]
(when (active? user)
(send-notification user)))
Summary
| Construct | Returns | Eager/Lazy | Primary Use | Side Effects? |
map | Transformed sequence | Lazy | Transform data | ❌ No (pure) |
for | Generated sequence | Lazy | Complex collection building | ❌ No (pure) |
doseq | nil | Eager | Multiple side effects per item | ✅ Yes |
run! | nil | Eager | Single function call per item | ✅ Yes |
Understanding the different iteration constructs in Clojure is crucial for writing efficient and effective code.
Use map and for when transforming data with lazy evaluation and pure functions. For producing side effects, doseq and run! are better suited, providing eager execution and clear intent for side-effecting operations.
I hope this helps on your Clojure journey. It has definitely helped in mine.
Further Reading Resources:



