Stumped with functional breadth-first tree traversal in Clojure?
Data Structures & Algorithms practice on Codemia
Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.
Introduction
Breadth-first traversal is easy to describe and easy to implement imperatively with a mutable queue. In Clojure, the functional version works best when you keep the same core idea but use an immutable queue, typically clojure.lang.PersistentQueue, instead of trying to force the problem into naive recursion over lists.
Why a Queue Is the Key Abstraction
Depth-first traversal fits plain recursion naturally because the next node to visit is always "the next child." Breadth-first traversal is different. You must remember the remaining siblings and cousins while you walk the current level.
That is a queue problem:
- take the next node from the front
- append its children to the back
- repeat until the queue is empty
Trying to write BFS without an explicit queue often leads to awkward concat chains and less clear code.
A Simple Tree Representation
We will represent a tree node as a map with a :value and a :children vector:
The expected breadth-first order is:
Idiomatic Functional BFS with loop and recur
In Clojure, loop and recur are still functional. They do not mutate state in place; they create efficient tail-recursive iteration with new bindings at each step.
This is usually the cleanest answer:
- '
peekreads the front of the queue' - '
popremoves the front' - '
reduce conjappends children to the back'
Nothing is mutated, but the algorithm still has the right shape for BFS.
Why concat Is Often the Wrong Tool
A first attempt at functional BFS often looks like:
That can work at a toy level, but it changes the data structure into a lazy sequence rather than preserving queue semantics explicitly. The code becomes less direct about what is happening, and performance characteristics become less obvious.
With PersistentQueue, the queue behavior is stated in the code instead of being simulated indirectly through list operations.
Returning Nodes Instead of Values
Sometimes you want the nodes themselves rather than just their :value fields. The traversal can be adjusted easily:
That keeps the traversal logic identical and only changes what gets accumulated.
A Lazy Version
If you want a lazy breadth-first sequence, you can wrap the same idea in lazy-seq:
This is useful when the consumer may not need the whole traversal immediately.
Why This Is Still Functional
Some developers worry that using loop means the solution is no longer functional. In Clojure, that is the wrong distinction. The key questions are:
- are you mutating shared state
- are your values immutable
- is the function referentially transparent
The queue in the example is immutable. Each step produces a new queue binding. That is still functional programming, just expressed in an efficient idiomatic form rather than as deeply nested recursion.
Common Pitfalls
The most common mistake is trying to write BFS with plain recursive descent over children and accidentally implementing depth-first traversal instead.
Another issue is using list operations such as concat without being clear about queue behavior. The code may work, but the intent becomes murkier and the cost model harder to reason about.
Developers also sometimes assume loop and recur are somehow unfunctional. In Clojure, they are standard tools for immutable iterative processes.
Finally, make sure every node has a :children collection, even if it is empty. If some nodes omit the key or use nil, your traversal should normalize that case or the reduce step may behave inconsistently.
Summary
- Functional BFS in Clojure is easiest when you model the algorithm explicitly with an immutable queue.
- '
clojure.lang.PersistentQueue/EMPTYis the standard queue starting point.' - '
loopandrecurare idiomatic and still functional in Clojure.' - Prefer queue operations such as
peek,pop, andconjover improvisedconcatchains. - A lazy breadth-first sequence is possible once the queue-based version is clear.
Related reading

DSA Fundamentals
Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Data Structures & Algorithms practice on Codemia
Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.