Search code examples
algorithmclojurefunctional-programmingside-effectsquil

Clojure Quil: nested loops while drawing lines


I'm having a serious problem with figuring out an algorithm in the Clojure Quil library. I've been trying to make some diagrams of networks/flocking patterns like this with the functional middleware mode. My question is how do you create a nested "for loop" in Quild in order to connect any given point to any other point with a line? The difference is between a set of points connected 1->2 3->4...etc and a set which is connected 1->2 1->3 1->4 2->3 2->4...etc.

My program looks something like this, though I've tried to edit it to be generic for the purpose of this question.

(ns stack-question.core
  (:require [quil.core :as q]
            [quil.middleware :as m]))   

;how to draw the point
(defn draw-points [point]
  (q/point (:x point) (:y point)))

;the main data structure
(defn make-point [num]
  {:name num
   :x (rand 500.0)
   :y (rand 500.0)})    

;draws connections between two objects
(defn connections [p1 p2]
      (q/stroke 200 200 100 150)
      (q/line (:x p1) (:y  p1) (:x p2) (:y p2)))))

(defn setup []
  (q/frame-rate 30)
  {:points (map make-points (take 1000 (range)))})

;this function isn't important to the question 
;but let's just say that the points move position each frame 
(defn update-state [state]
  (update state :points move-group))

;this is the problem function here.
;I'm trying to map over the collection, but with a fixed head
;and do so for each element in the collection
(defn make-conts [coll]
  (loop [x coll]
    (when (not (empty? (rest coll)))
      (map (partial connections (first x)) (rest x))
      (recur (rest x)))))


(defn draw-state [state]
  (do
    ;(q/frame-rate 1)
    (q/background 0)
    (doseq [x (map draw-point (:points state))] x)
    (doseq [x (make-conts (:points state))] x)))

(q/defsketch stack-question
  :title "GL"
  :size [500 500]
  :setup setup
  :update update-state
  :draw draw-state
  :middleware [m/fun-mode]
  :renderer :opengl)

I hope this isn't totally opaque.


Solution

  • There are two problems with your code:

    The first is that you are checking (rest coll) instead of (rest x) in the when statement, leading to an infinite loop.

    The second is that map returns a lazy sequence, meaning that the calls to connections is not taking place. This can be fixed by wrapping the call to map with doall, but as you are interested in the side effects, not the return values, you should use doseq instead.

    This version of the function (where the superfluous loop has also been removed) should work:

    (defn make-conts [coll]
        (when (not (empty? (rest coll)))
          (doseq [endpoint (rest coll)]
            (connections (first coll) endpoint))
          (recur (rest coll))))
    

    Instead of writing the code to generate the combinations yourself, you can use combinations from clojure.math.combinatorics:

    (require '[clojure.math.combinatorics :as combi])
    
    (defn make-conts [coll]
       (doseq [[p1 p2] (combi/combinations coll 2)]
         (connections p1 p2)))