I had a need today to take an existing map and put (assoc) a bunch of new key-value pairs into it. Additionally, many of these key-value pairs might have a nil value, in which case I’d prefer to skip the assoc. In particular, I didn’t want to overlay existing keys in the map with a nil, so it was not ok to just drop them and remove/ignore the nils later.
So I wrote this null-aware assoc called ?assoc (as ? is sometimes connected with null checking). Here was my cut:
(defn ?assoc "Same as assoc, but skip the assoc if v is nil" [m & kvs] (reduce #(let [[k v] %2] (if (not (nil? v)) (assoc %1 k v) %1)) m (partition 2 kvs)))
And you can see the difference in this simple example:
=> (assoc {} :a 1 :b nil :c 2) {:c 2, :b nil, :a 1} => (?assoc {} :a 1 :b nil :c 2) {:c 2, :a 1}
Since my colleagues are much smarter than me I threw it onto our chat room and Ryan came up with a much nicer solution to my eyes:
(defn ?assoc "Same as assoc, but skip the assoc if v is nil" [m & kvs] (->> kvs (partition 2) (filter second) flatten (apply assoc m)))
Understanding this requires being familiar with the -» threading macro which I touched on a bit in the past. The threading macros are a nice way to “turn around” a series of function calls that share the same argument so they read forward instead of backward.
The second example is really just a rewrite of the following identical code:
(defn ?assoc [m & kv] (apply assoc m (flatten (filter second (partition 2 kvs)))))
In this version you read bottom-up (or right to left) to follow the data flow. With the threading macro, the result of each step is fed in as the last argument to the next step in the flow. Depending on context, this can be easier to read.
As far as the implementation goes it has the following steps:
- (partition 2) – take a list of key-value pairs like (:a 1 :b 2 :c 3) and (lazily) partition into groups: ((:a 1) (:b 2) (:c 3))
- (filter second) – filter items in the list where the function (second %) returns true (nicely getting rid of the ones with nil values)
- (flatten) – un-partition back into a list like the original
- (apply assoc m) – applies the function (assoc) to the args m and the kv pairs