While working through some coding puzzles to brush up on Clojure, I encountered the need to use A* on some search problems. I tried to implement it from first principles/memory and that didn't go well, so I looked for existing implementations and found two (Joy of Clojure and cgrand's) but they were a little difficult to follow (for me) so I attempted to create my own. In the end it actually looks pretty close to cgrand's implementation.
I think this works as expected (using an example from here to verify), but just wanted to post to see if anybody can point out any glaring omissions / oversights. I've implemented this before in Python but it turned out to be quite different without using mutation.
The only issue I see is that I generate neighbors before testing whether I've already seen the node, so there is some inefficiency there. I couldn't figure out a good way to avoid this since my neighbor generation / filtering occurs in one step during the recursive call, whereas in languages allowing mutability you would process each child separately and you can test if it exists in the visited set each time.
(defn a-star-search
[start neighbor-func goal? remain-cost path-cost]
(loop [q (conj (sorted-set) [0 start])
cost-so-far {start 0}
came-from {start nil}]
(if-let [[node-cost node :as node-state] (first q)]
(if (goal? node)
(reverse (take-while (complement nil?) (iterate came-from node)))
(let [neighbors (neighbor-func node)
prev-node (came-from node)
prev-cost (cost-so-far node)
cheaper (remove #(< (cost-so-far % Double/POSITIVE_INFINITY)
(+ prev-cost (path-cost node %)))
neighbors)
new-nodes (map #(vector (+ prev-cost
(path-cost node %)
(remain-cost %)) %)
cheaper)]
(recur (into (disj q node-state) new-nodes)
(->> cheaper
(map #(vector % (+ prev-cost (path-cost node %))))
(into cost-so-far))
(into came-from (map (juxt identity (fn [_] node)) cheaper)))))
"no more neigbors")))
(def graph {:s {:a 1 :b 4}
:a {:b 2 :c 5 :g 12}
:b {:c 2}
:c {:g 3}})
(def h-vals {:s 7 :a 6 :b 2 :c 1 :g 0})
(a-star-search :s
#(-> % graph keys)
#(= :g %)
h-vals
(fn [from to] (get-in graph [from to])))