How would I code the equivalent of the following python in clojure, but strictly using recursion for the stack management (eg not using a loop/recur with a vector acting as the frontier)?
I realize it's fairly simple with a vector maintaining your paths and just peeking / popping, but I'm doing this as a thought exercise.
Python Version
def dfs(start,successors,goal,visited=set()):
if start not in visited:
visited.add(start)
for s in successors.get(start):
if goal(s):
return s
else:
res = dfs(s,successors)
if res: return res #bail early when found
return False
Clojure Version
(defn dfs [start goal? successors visited]
(if (goal? start)
start
(when (not (contains? visited start))
(mapcat #(dfs % goal? successors (conj visited start))
(successors start)))))
Since iteration is controlled by a call to map in the Clojure version, you can't really bail early the way you can in Python eg if goal(s): return s
.
Since you are collecting the recursive calls inside of a list with map, you are forced to evaluate every possible node even after the target is found. Only then after all nodes have been explored do you get the result.
(defn dfs-non-rec [frontier goal? successors visited] (loop [f frontier g? goal? s successors v visited] (let [node (peek f)] (cond ; case 1 (goal? node) node ;case 2 (not (contains? v node)) (recur (vec (concat (pop f) (successors node))) g? s (conj v node)) ;case 3 :else (recur (pop f) g? s (conj v node))))))
How should I approach this?
EDIT
(def graph {"a" ["b","c","d"], "b" ["a","e","f"], "c" ["x","y"], "d" [], "e" [], "f" [], "x" ["c"], "y" ["e"]})
then when converted to a seq, the order followed is in-fact depth-first for that resulting tree created by calling seq on the graph, however the order implied by the adjacency list is not followed, because the graph structure is lost in the conversion.
So if you are looking for the node x
starting at the a
, I would expect the traversal order to be adcyex
, rather than abcdbaefcxy
first of all you don't really need to check tree for loops, since clojure's data structures don't have circular references (unless you don't use mutable state with atom
s referencing to another atoms, which is an obvious code smell). The simple way of traversal could look like this (this way is referenced by lots of lisp (and overall programming) books):
user> (defn dfs [goal? data]
(if (goal? data)
data
(loop [data data]
(when-let [[x & xs] (seq data)]
(cond (goal? x) x
(coll? x) (recur (concat x xs))
:else (recur xs))))))
user> (dfs #{10} [1 [3 5 [7 9] [10] 11 12]])
10
user> (dfs #{100} [1 [3 5 [7 9] [10] 11 12]])
nil
in addition there are more concise (and thus idiomatic i guess) ways to do this in clojure. The simplest one is to use tree-seq :
user> (defn dfs [goal? tree]
(first (filter goal? (tree-seq coll? seq tree))))
#'user/dfs
user> (dfs #{10} [1 [3 5 [7 9] [10] 11 12]])
10
user> (dfs #{100} [1 [3 5 [7 9] [10] 11 12]])
nil
user> (dfs (every-pred number? even?) [1 [3 5 [7 9] [10] 11 12]])
10
tree-seq
is lazy, so it only traverses tree until you find the needed value.
another way is to use clojure's zippers :
user> (require '[clojure.zip :as z])
nil
user> (defn dfs [goal? tree]
(loop [curr (z/zipper coll? seq identity tree)]
(cond (z/end? curr) nil
(goal? (z/node curr)) (z/node curr)
:else (recur (z/next curr)))))
#'user/dfs
user> (dfs #{10} [1 [3 5 [7 9] [10] 11 12]])
10
user> (dfs #{100} [1 [3 5 [7 9] [10] 11 12]])
nil
user> (dfs (every-pred number? even?) [1 [3 5 [7 9] [10] 11 12]])
10
I would do it like the following, which uses the Tupelo library for testing:
(ns tst.demo.core
(:use tupelo.test)
(:require [tupelo.core :as t] ))
(def data [[1 2 [3]]
[[4 5] 6]
[7]])
(def search-result (atom nil))
(defn search-impl
[goal? data]
(when (= ::not-found @search-result)
(if (goal? data)
(reset! search-result data)
(when (coll? data)
(doseq [it data]
(search-impl goal? it))))))
(defn search [goal? data]
(reset! search-result ::not-found)
(search-impl goal? data)
@search-result)
(dotest
(println "1 => " (search #(= 5 %) data))
(println "2 => " (search #(and (integer? %) (even? %)) data))
(println "3 => " (search #(= [4 5] %) data))
(println "4 => " (search #(= 99 %) data)) )
with results:
1 => 5
2 => 2
3 => [4 5]
4 => :tst.demo.core/not-found
Don't be afraid to use a bit of mutable state (in this case an atom) when it makes your program clearer and/or simpler.
If you really want to hide the atom from being globally visible, just do this:
(defn search2-impl
[search2-result goal? data]
(when (= ::not-found @search2-result)
(if (goal? data)
(reset! search2-result data)
(when (coll? data)
(doseq [it data]
(search2-impl search2-result goal? it))))))
(defn search2 [goal? data]
(let [search2-result (atom ::not-found)]
(search2-impl search2-result goal? data)
@search2-result))
(dotest
(println "21 => " (search2 #(= 5 %) data))
(println "22 => " (search2 #(and (integer? %) (even? %)) data))
(println "23 => " (search2 #(= [4 5] %) data))
(println "24 => " (search2 #(= 99 %) data)))
21 => 5
22 => 2
23 => [4 5]
24 => :tst.demo.core/not-found
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.