Skip to main content

Command Palette

Search for a command to run...

Mastering Clojure Iteration: A Practical Guide

When to use for, map, doseq, and run!

Updated
5 min read
Mastering Clojure Iteration: A Practical Guide
K

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?

  • 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

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 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)

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 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)))

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

ConstructReturnsEager/LazyPrimary UseSide Effects?
mapTransformed sequenceLazyTransform data❌ No (pure)
forGenerated sequenceLazyComplex collection building❌ No (pure)
doseqnilEagerMultiple side effects per item✅ Yes
run!nilEagerSingle 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: