Search code examples
clojurefunctional-programming

What's the functional version of a nested test?


I'm converting some C++ code to Clojure, and I want to return a graph g with a bunch of edges added to it. I pass in the the number of vertices, the graph, and the test predicate (eg, a function that could depend on i, j, randomness, ...) something like this:

(defn addSomeEdges [v g test-p]
  (doseq [i (range v)]
    (doseq [j (range (dec i))] 
      (if test-p 
          (add-edges g [i j] )
          )))
  g)

the problem, of course, is that (add-edges) returns a new g. How can I capture this updated graph using best practices Clojure, please? It seems so simple and natural in C++.


Solution

  • Iterativly accumulating information looks like a reducing function if you split it into two parts:

    • Generate a bunch of edges to consider including.
    • Test each edge and if it passes, include it. Otherwise pass the result on unchanged

    Which can be written using reduce

    user> (defn add-edge [g i j]
            (assoc g i j))
    #'user/add-edge
    
    user> (add-edge {1 2} 2 1)
    {1 2, 2 1}
    
    user> (defn addSomeEdges [v g test-p]
            (reduce (fn [graph [i j]]        ;; this takes the current graph, the points,
                      (if (test-p graph i j) ;; decides if the edge should be created.
                        (add-edge graph i j) ;; and returns the next graph
                        graph))              ;; or returns the graph unchanged.
                    g  ;; This is the initial graph
                    (for [i (range v)    
                          j (range (dec i))]
                      [i j])))  ;; this generates the candidate edges to check.
    #'user/addSomeEdges
    

    and let's run it!

    user> (addSomeEdges 4 {1 2} (fn [g i j] (rand-nth [true false])))
    {1 2, 2 0}
    user> (addSomeEdges 4 {1 2} (fn [g i j] (rand-nth [true false])))
    {1 2, 3 0}
    user> (addSomeEdges 4 {1 2} (fn [g i j] (rand-nth [true false])))
    {1 2, 2 0, 3 1} 
    

    When you think of other tests you can thread these calls together:

    user> (as-> {1 2} g
            (addSomeEdges 4 g (fn [g i j] (rand-nth [true false])))
            (addSomeEdges 7 g (fn [g i j] (< i j)))
            (addSomeEdges 9 g (fn [g i j] (contains? (set (keys g)) j))))
    {1 2, 3 1, 4 1, 5 3, 6 4, 7 5, 8 6}