Clojure is a new lisp-like programming language, that runs on JVM. It has tons of interesting features that are worth exploring. To name a few, concurrency model based on immutable data and software transactional memory, lazy sequences, etc.
I am currently re-reading a book on algorithms, so I decided that it might be a good idea to implement some of these algorithms on Clojure. I have implemented breadth and depth first graph traversals, using lazy-seq paradigm.
Some aspects of the implementation may appear to be slightly inefficient; for example I represent graph as a sequence of pairs, and each time I need to get all the neighbors of a node I traverse the entire sequence. But I did it that way so that I could concentrate on the algorithm at question.
For the experienced reader, if there is anything in my code that you think may be considered as a bad tone, please don't hesitate to comment.
Without further ado, here it is:
(ns algo.traversal)
"
1--2-6--7
| \\ /
3--4-5
"
(def graph (seq {1 2, 1 3, 2 6, 6 7, 3 4, 4 5, 2 5, 5 7}))
(defn transitions
"Finds all the transitions going to and from the node.
Transition to the node are reversed."
[graph node]
(map #(if (= node (first %))
%
(reverse %))
(filter #(or (= node (first %)) (= node (second %))) graph)))
(defn breadth-first
"Returns a lazy sequence of transitions in breadth-first order
starting from the given node."
[graph start]
(let [walk
(fn walk [past-nodes trans]
(let [next-trans (drop-while #(past-nodes (second %)) trans)]
(when-let [next-node (second (first next-trans))]
(lazy-seq
(cons (first next-trans)
(walk (conj past-nodes next-node)
(concat next-trans (transitions graph next-node))))))))]
(walk #{start} (transitions graph start))))
(defn depth-first
"Returns a lazy sequence of transitions in depth-first order
starting from the given node."
[graph start]
(let [walk
(fn walk [past-nodes trans]
(let [next-trans (drop-while #(past-nodes (second %)) trans)]
(when-let [next-node (second (first next-trans))]
(lazy-seq
(cons (first next-trans)
(walk (conj past-nodes next-node)
(concat (transitions graph next-node) next-trans)))))))]
(walk #{start} (transitions graph start))))
As I mentioned before I define graph as a sequence of pairs, all the transitions are navigable in both directions.
breadth-first and depth-first functions are almost the same except for one tiny part; I will talk about it later.
Now, let's take a closer look at the structure of these functions.
Both of them do two things: define inner function walk and call it. Apparently all juice is in this function. Here's the one from breadth-first:
(fn walk [past-nodes trans]
(let [next-trans (drop-while #(past-nodes (second %)) trans)]
(when-let [next-node (second (first next-trans))]
(lazy-seq
(cons (first next-trans)
(walk (conj past-nodes next-node)
(concat next-trans (transitions graph next-node))))))))
walk is used to recursively traverse the graph. It is closed around the outer function arguments [graph start], and it has two of its own arguments:
- past-nodes - hash-set of nodes that have already been traversed and
- trans - transitions yet to walk.
Next line defines a var next-trans, which is a sequence of transitions. It is guaranteed to start with a transition to a new non-traversed node.
next-trans (drop-while #(past-nodes (second %)) trans)After that next-node var is assigned the next node id. If next-trans is empty the whole block will return nil.
when-let [next-node (second (first next-trans))]Following is the call to lazy-seq, this function wraps its arguments in a lazy sequence, which allows Clojure stop the computation until further data is requested. I pass it a sequence with its elements being: the next transition and a recursive call to walk.
The recursive call represents the sole difference between the two algorithms. In the case of breadth-first I build the sequence of transitions to traverse by appending transitions from the current node to the end of the sequence,
(walk (conj past-nodes next-node)
(concat next-trans (transitions graph next-node)))
when in the depth-first case I do the opposite by appending to the head of the sequence:
(walk (conj past-nodes next-node)
(concat (transitions graph next-node) next-trans))