Learn how `filter` keeps matching items in a Clojure pipeline, how laziness affects it, and how it differs from Java loops and stream filtering.
filter is the function you use when the collection shape stays the same but only some items should survive.
For Java developers, the reliable mental model is:
map changes every itemfilter decides which items stayreduce combines items into one resultIf you keep those jobs distinct, collection pipelines become much easier to read.
filter Doesfilter takes:
and returns a lazy sequence containing only the items for which the predicate is truthy.
1(filter even? [1 2 3 4 5 6])
2;; => (2 4 6)
The predicate is just the rule for keeping an item.
Suppose you want only billable time entries.
1(defn billable? [entry]
2 (and (:time-entry/approved? entry)
3 (not (:time-entry/internal? entry))))
4
5(defn billable-entries [entries]
6 (->> entries
7 (filter billable?)
8 (into [])))
This reads well because the rule has a clear name and the pipeline says exactly what happens.
That is the main design benefit of filter: it turns selection logic into an explicit, reusable rule.
filter Versus Java Habits| Java habit | Clojure shape |
|---|---|
Loop with if before adding to a result list |
filter plus a predicate |
.stream().filter(...) |
(filter pred coll) |
| Boolean-heavy selection buried inside traversal code | Named predicate passed into the pipeline |
The syntax difference is minor. The more important difference is that Clojure encourages you to make the selection rule a first-class function instead of letting it disappear into loop machinery.
filter itself is simple. Most of the code quality lives in the predicate you pass to it.
Good predicates:
1(defn overdue? [invoice]
2 (and (= :open (:invoice/status invoice))
3 (> (:invoice/days-late invoice) 30)))
Weak predicates usually try to smuggle too much business logic into one anonymous function.
Like map, filter returns a lazy sequence.
That means:
This is often efficient, but it matters around resources and side effects.
If you filter lines from a file inside with-open, for example, you usually need to realize the result before the reader closes.
filter, remove, and keepThese are related but not interchangeable.
| Function | Use it when… |
|---|---|
filter |
You want to keep items that satisfy a predicate |
remove |
You want to drop items that satisfy a predicate |
keep |
You want to transform items and drop nil results |
This is a common source of muddy code. If the operation is “transform and maybe keep”, keep may be a better fit than forcing the logic into filter.
A common and readable shape is:
1(->> invoices
2 (filter overdue?)
3 (map :invoice/id)
4 (into []))
This works well because each step has one job:
filter chooses survivorsmap transforms survivorsMany awkward pipelines come from collapsing both concerns into one step.
| Mistake | Why it hurts | Better move |
|---|---|---|
Using filter to transform values |
The predicate should decide keep/drop, not reshape data | Use map after filter |
| Returning non-boolean values without clear intent | Truthiness works, but the rule becomes harder to read | Prefer predicates that make the selection rule obvious |
| Forgetting laziness around resources | Evaluation may happen later than you expect | Realize the sequence when needed |
| Embedding a large anonymous predicate in the pipeline | The business rule becomes harder to review | Name the predicate |