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 😃
Search for a command to run...
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 😃
No comments yet. Be the first to comment.
Property based testing leading to unexpected URL discoveries

Some Takeaways from AI-assisted Rescript Exploration

Do you believe your job can be done by AI? Up until very recently I believed this was all hype. No way this could happen. There have always been proclamations of new technologies like AI taking jobs. Most of the time dismissed as fantasy, most takes ...

Return the most frequently used terminal commands

You're working with a collection in Clojure and need to iterate over it. You reach for... wait, which one?
for
map
doseq
run!
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
map - Transform CollectionsPurpose: 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)
for - List ComprehensionsPurpose: 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 let bindings inside the iteration
You'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)
doseq - Imperative Iteration for Side EffectsPurpose: 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 for loop 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)))
run! - Concise Side EffectsPurpose: 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)
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)))
| 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: