Clojure noob在这里。
我想拉开前面,然后休息一下。执行(front #{1})
和(rest #{1})
分别产生1
和()
,这主要是我所期望的。
但是在下面的代码中,我在循环中使用解构[current-node & open-nodes] #{start}
从集合中取出一些东西(此时我并不关心它是第一个还是最后一个项目。我只是希望这个表格正常工作)并且它会中断。
这是我的功能,半实现网格搜索:
(defn navigate-to [grid start dest]
"provides route from start to dest, not including start"
(loop [[current-node & open-nodes] #{start} ;; << throws exception
closed-nodes #{}]
(if (= dest current-node)
[] ;; todo: return route
(let [all-current-neighbours (neighbours-of grid current-node) ;; << returns a set
open-neighbours (set/difference all-current-neighbours closed-nodes)]
(recur (set/union open-nodes open-neighbours)
(conj closed-nodes current-node))))))
当单步执行时(使用Cider),在第一个循环开始时,它会抛出此异常:
UnsupportedOperationException nth not supported on this type: PersistentHashSet clojure.lang.RT.nthFrom (RT.java:933)
我可以使用嵌套的let表单,它首先/手动执行,但这看起来很浪费。有没有办法让循环形式的destructured集合像这样工作?是不是套装不支持?
答案 0 :(得分:3)
集合是无序的,因此位置解构没有多大意义。
根据同样处理解构的Special Forms文档,指定顺序(向量)绑定使用nth
和nthnext
来查找要绑定的元素。
Vector binding-exprs允许您将名称绑定到顺序事物(不仅仅是矢量)的部分,例如矢量,列表,序列,字符串,数组以及支持第n个的任何内容。
Clojure哈希集(是java.util.Set
的实例)不支持按索引查找。
我不知道您的示例代码的上下文,但无论如何将设置内容倾注到有序集合中,例如(vec #{start})
,都会使解构工作。
答案 1 :(得分:2)
正如其他人所提到的,您无法将集合绑定到矢量文字,因为集合不是顺序的。因此即使这个简单的let
也会失败, nth不受支持:
(let [[x] #{1}])
您可以通过使用first
和disj
“解构”该集来解决此问题:
(loop [remaining-nodes #{start}
closed-nodes #{}]
(let [current-node (first remaining-nodes)
open-nodes (disj remaining-nodes current-node)]
;; rest of your code ...
))
使用(rest remaining-nodes)
代替(disj remaining-nodes current-node)
是可能的,但由于集合是无序的,理论上rest
没有义务取出与{{1}提取的元素相同的元素}。无论如何first
将完成这项工作。
注意:请务必检测剩余节点 nil ,这可能导致无限循环。
为了实现算法中的缺失部分(返回路线),您可以维护 路径图。每个被访问节点都有一条路径:一个向量,节点从起始节点通向该节点,由该节点键入。
您可以在访问新节点时使用disj
来维护该路径图。使用与reduce
一起使用的新函数和添加的 nil 测试,程序可能如下所示:
reduce
虽然我将原始(defn add-path [[path paths] node]
"adds a node to a given path, which is added to a map of paths, keyed by that node"
[path (assoc paths node (conj path node))])
(defn navigate-to [grid start dest]
"provides route from start to dest, including both"
(loop [remaining-nodes #{start}
closed-nodes #{}
paths (hash-map start [start])]
(let [current-node (first remaining-nodes)
current-path (get paths current-node)
all-current-neighbours (neighbours-of grid current-node)
open-neighbours (set/difference all-current-neighbours closed-nodes)]
(if (contains? #{dest nil} current-node)
current-path ;; search complete
(recur (set/union (disj remaining-nodes current-node) open-neighbours)
(conj closed-nodes current-node)
(second (reduce add-path [current-path paths] open-neighbours)))))))
与解构节点所需的原始let
合并,但算法的本质仍然相同。这不是绝对必要的,但它可能使代码更具可读性。
我使用 grid 和 neighbors-of 的穷人定义对此进行了测试,基于此图表(数字是节点,条形表示链接节点:
0--1 2
| | |
3--4--5
|
6--7--8
这个图表似乎是测试的一个很好的候选者,因为它有一个循环,一个死角,并且是连接的。
图形使用 grid 作为向量进行编码,其中每个元素代表一个节点。该向量中的元素索引是节点的标识符。每个元素的内容都是一组邻居,使邻居 - 的功能变得微不足道(你的实现会有所不同):
(def grid [#{1 3} #{0 4} #{5}
#{0 4 6} #{1 3 5} #{2 4}
#{3 7} #{6 8} #{7} ])
(defn neighbours-of [grid node]
(get grid node))
然后测试是找到从节点0到节点8的路由:
(println (navigate-to grid 0 8))
输出是:
[0 1 4 3 6 7 8]
这个结果表明算法不保证最短的路线,只有路线才能找到。我想在不同引擎上的结果可能会有所不同,具体取决于Conjure内部如何决定从first
的集合中采用哪个元素。
删除其中一个必需的节点链接(如节点7和8之间的节点链接)后,输出为 nil 。
注意:我发现这是一个有趣的问题,在我的回答中可能有点太过分了。