【问题标题】:How to handle multiple variables in a Clojure algorithm implementation?如何在 Clojure 算法实现中处理多个变量?
【发布时间】:2019-04-04 04:35:18
【问题描述】:

我是 Clojure 的新手,并试图通过在其中实现一些算法来学习。我正在编写的算法用于计算图数据结构的节点 betweenness centrality 指标。

我尝试实现的算法(Brandes 算法)中的函数如下所示:

这里,V 是图的顶点,s 是我们尝试计算并返回最短路径度量的起始节点S, Pred and sigma

这是我通过使用loom 为每个起始节点g 创建初始图start 所设法提出的:

 (defn ss-shortest-path     
   [g start]   
    (let [nodeset (disj (nodes g) start)
        pred (apply assoc {} (interleave (nodes g) (repeat nil)))
        dist (apply assoc {start 0} (interleave nodeset (repeat -1)))
        sigma (apply assoc {start 1} (interleave nodeset (repeat 0)))
        stack []]
    (loop [queue (conj clojure.lang.PersistentQueue/EMPTY start)]
      (if (empty? queue)
        {:sigma sigma
         :pred pred
         :stack stack}
        (let [v (peek queue)
              stack (conj stack v)]
          (doseq [w (successors g v)]
            (when (= (dist w) -1)
              (do
                (conj queue w)
                (assoc dist w (+ 1 (dist v)))))
            (when (= (dist w) (+ 1 (dist v)))
                  (do
                    (assoc sigma w (+ (sigma w) (sigma v)))
                    (assoc pred w v))))
            (recur (pop queue)))))))

我知道 Clojure 数据结构是不可变的,因此每次我在变量 pred, sigma, stack, dist 中调用 conjassoc 时都会创建一个新副本,并且原始变量保持不变。

但是,我不想使用像atomsrefs 这样的可变状态,因为我觉得这只是在复制我已经知道的命令式样式。

因此,我正在寻求一些经验丰富的 Clojurists 的帮助,以帮助我以惯用的方式创建此功能。

提前致谢。

【问题讨论】:

    标签: algorithm recursion clojure functional-programming immutability


    【解决方案1】:

    我会做两件主要的事情:首先,算法的状态由多个“变量”(queuestack 等)组成。我将首先使用不可变映射构造一个表示算法状态的函数,例如

    (defn initialize-state [g start]
      (let [nodeset (disj (nodes g) start)]
        {:g g
         :nodeset nodeset
         :pred (apply assoc {} (interleave (nodes g) (repeat nil)))
         :dist (apply assoc {start 0} (interleave nodeset (repeat -1)))
         :sigma (apply assoc {start 1} (interleave nodeset (repeat 0)))
         :stack []
         :queue (conj clojure.lang.PersistentQueue/EMPTY start)
         :current-vertex nil}))
    

    然后,我会在 REPL 中测试该映射是否针对 gstart 的各种选择正确初始化。

    第二,我会将算法分解为多个小函数,这些函数以 state 作为输入并返回 state 作为输出,例如这个(这段代码行不通,你必须填写缺失的部分):

    (defn next-vertex [state]
      {:pre [(state? state)]
       :post [(state? %)]}
      (let [v (peek (:queue state))]
        (-> state
            (update :stack conj v)
            (assoc :current-vertex v))))
    
    (defn process-successor [state w]
      (let [dist-w (dist w)]
        (cond
          ;; fill in...
          )))
    
    (defn process-successors [state]
      {:pre [(state? state)]
       :post [(state? %)]}
      (reduce
       process-successor
       state
       (successors (:g state) (:current-vertex state))))
    
    (defn pop-queue [state]
      {:pre [(state? state)]
       :post [(state? %)]}
      (update state :queue pop))
    

    带有:pre:post 键的映射是所谓的前置条件和后置条件,state? 功能可以实现,例如,(defn state? [x] (and (map? x) (contains? x :queue))),作为健全性检查。

    请注意,对于您编写的每个函数,您都可以在 REPL 中使用一些数据对其进行测试,以确保它可以正常工作,然后再编写下一个函数。现在可以使用comp 将所有这些函数包装在一起形成一个完整的状态转换:

    (def next-state (comp pop-queue process-successors next-vertex))
    

    现在最终的算法是这样的:

    (defn ss-shortest-path [g start]
      (loop [state (initialize-state g start)]
        (if (empty? (:queue state))
          state
          (recur (next-state state)))))
    

    总而言之,如果您将算法分解为可以单独开发和验证的更小部分,那么实现算法会容易得多。

    【讨论】:

    • 这非常棒。正是我想要的。我能够完成代码并且它有效!还有一个关于 ":pre" 和 ":post" 映射的问题。那些用于什么以及如何定义函数“状态?”。如果您能解释一下,将不胜感激。再次感谢您的出色回答。
    • 很高兴我能帮上忙! "{:pre ... :post ...}" 是所谓的前置条件和后置条件,在此解释:clojure.org/reference/…。和“状态?”只是一个检查数据是否与我们选择的“状态”表示相对应的函数,例如,您可以将其实现为 (defn state?[x] (and (map?x) (contains?x :queue))。您不必使用前置条件和后置条件,很多人不需要。但我发现使用断言和前置条件和后置条件有助于检测和定位错误。
    【解决方案2】:

    其他答案都没有明确说明这一点,所以我想我会澄清“处理不变性”部分。

    我想说loop 是在这里使用的正确构造。您设置的问题是您的循环唯一的累加器是queue。从一次迭代到下一次迭代的每一位数据都应该是循环累加器的一部分。

    在您的情况下,distsigmapredstack 都是有可能从一个循环迭代更改为下一个循环迭代的数据,因此它们都应该在方括号中声明的循环。然后,当您需要更新其中一条数据时,您可以更新提供给recur 的内容:

    (loop [queue (conj clojure.lang.PersistentQueue/EMPTY start)
           pred (apply assoc {} (interleave (nodes g) (repeat nil)))
           dist (apply assoc {start 0} (interleave nodeset (repeat -1)))
           sigma (apply assoc {start 1} (interleave nodeset (repeat 0)))
           stack []]
    
      (if (empty? queue)
        (Return everything)
        (recur (conj queue some-data)
               (assoc pred :some-key some-data)
               (assoc dist :another-key other-data)
               (assoc sigma :key data)
               (conj stack stack-data))))
    

    提供给recur(本例中为更新的不可变结构)的所有内容都将在下一次迭代中提供给loop

    我同意@Ru​​lle 的观点,尽管你有这么多累加器,将它们全部打包到自己的结构中比手动处理每次迭代要简洁得多。

    【讨论】:

      【解决方案3】:

      背景:这里是一个java版本的alg:https://github.com/jgrapht/jgrapht/blob/master/jgrapht-core/src/main/java/org/jgrapht/alg/scoring/BetweennessCentrality.java


      首先,您需要定义 s、Pred、sigma。您还应该为 g、v、start 等定义格式。

      其次,我不确定这是最好的学习练习。您可以用 Clojure loop/recurdoseq 等替换 Java whilefor 等,但它仍然感觉像是“强制适配”。通过阅读“Clojure for the Brave & True”、“Getting Clojure”等好书,您可能会更快地学习更多(并且更深入!)。

      我们的想法是,小而独立的练习问题比单个庞大的问题更有效地学习。


      别忘了收藏:

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-01-30
        • 2022-11-02
        • 2020-08-15
        • 2016-06-03
        • 1970-01-01
        • 2021-05-21
        相关资源
        最近更新 更多