How to make stateful Clojure code pure without excessive repetition?

107 Views Asked by At

I'm writing a Clojure library to generate Voronoi diagrams using the quad-edge data structure as described in this research paper. I was using refs to represent the edge records, but I wanted to change the code to be pure for a couple reasons:

  • I don't want users of the library to have to worry about details like wrapping their code in a dosync.
  • I want to be able to make non-destructive updates, such as clipping a diagram to a viewport without altering the original triangulation.
  • Refs are not implemented in Clojurescript.

The problem is that the pure code is extremely repetitive due to passing around the data structure to every single function. Here's an example:

(defn- bubble-up [qeds cross]
  (loop [qeds qeds
         cross cross]
    (let [l-edge (first-hit qeds cross (q/r-prev qeds cross) q/o-next)
          r-edge (first-hit qeds cross (q/o-prev qeds cross) q/o-prev)
          l-valid (above? qeds l-edge cross)
          r-valid (above? qeds r-edge cross)]
      (when (or l-valid r-valid)
        (let [[qeds new-cross]
              (if (or (not l-valid)
                      (and r-valid
                           (in-circle? (dest qeds l-edge) (org qeds l-edge) (org qeds r-edge)
                                       (dest qeds r-edge))))
                (connect qeds r-edge (q/sym cross))
                (connect qeds (q/sym cross) (q/sym l-edge)))]
          (recur qeds new-cross))))))

I've tried various other approaches, including:

  • Writing the mutable part in Java.
  • Using a state monad (which is slightly better but still clunky and not idiomatic.)
  • Putting the data structure in a dynamic var.

Is there a standard, idiomatic way to deal with a situation like this in Clojure(script)?

3

There are 3 best solutions below

0
Eugene Pakhomov On

Is there a standard, idiomatic way to deal with a situation like this in Clojure(script)?

Can't judge for all, but I would leave things as they are. And at the risk of stating the obvious, if some particular set of forms (that's not just a single function call) can be seen in more than 2 places, it probably deserves to be extracted into its own function. But the presented code doesn't have something like that.

If you dislike that repetition to such an extent that something must be done, as an alternative you can create a function that's like partial but upside-down:

(defn bind [obj & fns]
  (mapv #(prepare % obj) fns))

And then use it as:

(let [[first-hit r-prev o-prev above?]
      (bind qeds first-hit o/r-prev o/o-prev above?)]

      l-edge (first-hit cross (r-prev cross) q/o-next)
      r-edge (first-hit cross (o-prev cross) q/o-prev)
      l-valid (above? l-edge cross)
      r-valid (above? r-edge cross)]
  ...)

Whether it's worth it or not is up to you. I probably wouldn't do it, unless there were many more similar lines with repeated usages of the same functions.

As a yet another alternative, you could use a dynamic variable to store the graph. But in some regards it might be even worse than a refs/atoms-based solution.

0
Alan Thompson On

Your use case is a good candidate for using a dynamic Var. A good example can be seen in the Tupelo Forest library. In cases like this, a global variable can reduce clutter in function calls.

However, global variables are often a bad idea. Their use hides dependencies between different functions. This can result in concise, tight code that is easier to read. However, it assumes you know about the coupling due to the global variable.

Here is a good discussion of the cost/benefit tradeoff for dynamic Vars.

My conclusion? Sometimes the benefit outweighs the cost, other times it doesn't. For my limited understanding of your use case, I would use a dynamic Var. If you change your mind later, then change it back to an explicit function parameter.


P.S. When I do pass explicit params through a call chain, I usually wrap them all up into a single map named ctx (a short semi-standard name for "context"), and pass that as the first parameter to each function.

0
Thumbnail On

You can use mapv to factor common code:

(defn- bubble-up [qeds cross]
  (let [[l-edge r-edge] (mapv #(first-hit qeds cross (q/r-prev qeds cross) %) [q/o-next q/o-prev])
        [l-valid r-valid] (mapv #(above? qeds % cross) [l-edge r-edge])]
    (when (or l-valid r-valid)
      (let [right-handed? (if (or (not l-valid)
                                  (and r-valid
                                       (in-circle? (dest qeds l-edge) (org qeds l-edge) (org qeds r-edge) (dest qeds r-edge)))))
            [new-qeds new-cross] (if right-handed?
                                   (connect qeds r-edge (q/sym cross))
                                   (connect qeds (q/sym cross) (q/sym l-edge)))]
        (recur new-qeds new-cross)))))

Other simplifications:

  • There is no need for the loop. The function bubble-up is a recur target.
  • Extract the complex condition into a local binding.

None of this addresses the major problem: qeds is passed in as an argument to every function, and - worse - has to be passed out again by connect in a data structure.

Zach Tellman wrote Proteus to provide efficient (local) mutable bindings. It no longer works.