Pure Danger Tech


navigation
home

Clojure code review cleanup

23 Feb 2010

Did some code review yesterday on some recent Clojure code and wanted to list one simple example of some ugly stuff we cleaned up a bit.

I had some code like this:

(defn check-prop-bridges [model]
    (let [class-maps (class-map-names model)
          prop-bridges (prop-bridge-mapping model)]
     
         (map #(bad-class-map-err (first %) (second %))
             (filter #(not (is-class-map? (second %) class-maps)) 
                 prop-bridges))))

I’m skipping lots of supporting code here but in essence the class-map-names function is returning a list of all known names and prop-bridge-mapping is returning a list of vectors in the form [prop-bridge-name class-map-name]. And the point of this code is to look through all of the mappings, find mappings where the class-map-name is unknown, and generate an error for each of those.

I was accomplishing that by first doing a filter to remove good mappings, then using map to generate an error for each remaining bad mapping.

The main thing that was smelly here was all the first and second business. 1) it’s kind of ugly and verbose, 2) there are two functions relying on the positional nature of the data returned, 3) this code is implicitly coupled to the prop-bridge-mapping function creating that intermediate data structure.

The key insight in cleaning this up a bit was to use destructuring in a list comprehension (for) to make the ordering of the passed data more explicit AND perform the filter and map in a single conceptual step:

(for [[p c] prop-bridges 
                 :when (not (is-class-map? c class-maps))]
              (bad-class-map-err p c))))

The list comprehension has the documentation:

user=> (doc for)
-------------------------
clojure.core/for
([seq-exprs body-expr])
Macro
  List comprehension. Takes a vector of one or more
   binding-form/collection-expr pairs, each followed by zero or more
   modifiers, and yields a lazy sequence of evaluations of expr.
   Collections are iterated in a nested fashion, rightmost fastest,
   and nested coll-exprs can refer to bindings created in prior
   binding-forms.  Supported modifiers are: :let [binding-form expr ...],
   :while test, :when test.

That is, something like:

(for [stooge ["Larry" "Curly" "Moe"]]
	(hit-with-pipe stooge))

can be read “for each stooge in this sequence of stooges, perform the hit-with-pipe action.” In the [ ] you see the variable and the sequence from which it is taken (and there can be multiple variables as well).

Going back to our refactored code above, we see that the variable is not just a mapping but actually [p c]. This is destructuring and actually defines the structure of the expected mapping AND binds the first and second elements to p and c.

Additionally, the filtering operation is occurring during the list comprehension using a :when modifier. The body-expr will only be evaluated when the :when modifier returns true.