Welcome! Please see the About page for a little more info on how this works.

+9 votes
in Collections by
retagged by

I do lots of work with spec, and a frequent problem that I run into is in constructing a spec'ed object that is a map with optional fields.

A common pattern looks like this:

(assoc {::required-field :value}
       ::optional-field (when some-condition

There is a significant issue with this pattern however: if ::optional-field is on a spec/keys :opt field that is non-nilable, then every object that excludes the optional field will be invalid according to that spec because it will have a nil value instead of omitting the key.

The alternative to this approach is to use something like cond-> as seen below:

(cond-> {::required-field :value}
  some-condition (assoc ::optional-field :optional-value))

This pattern is far from the only usage of cond->, but it appears fairly frequently. I see this pattern as detrimental because it requires the condition to be surfaced all the way at the top here, and there cannot be a way for some function used to produce :optional-value to abort via nil punning while still producing a valid value without requiring additional flow control and local bindings.

A more flexible solution to this problem uses if-some, but this solution is verbose and becomes unwieldy when multiple optional fields need to be included at once, as can be seen below.

(let [m {::required-field :value}
      m (if-some [v (produces-optional-value)]
          (assoc m ::optional-field v)
      m (if-some [v (produces-other-value)]
          (assoc m ::other-field v)

One solution to this problem is the function assoc-some, which is like assoc, but when given a nil value it elides the key. This function is provided in medley, and sees some use.

The above unwieldy example would, with assoc-some, look like this:

(assoc-some {::required-field :value}
            ::optional-field (produces-optional-value)
            ::other-field (produces-other-value))

This is likely not the only solution to this problem, but I think it is a problem worth considering.

An example of another instance where the named solution wouldn't work, but that appears to follow more or less the same pattern is this code in farolero, which would be solved by a parallel function (that I have not yet found an implementation of in the wild besides my own) update-some which will update a key with a new value, and if that value is nil will remove the key.

1 Answer

+1 vote