Avoid parameter lists with more than three or four positional parameters.
I recently wrote a function that requires a 5 values and also can use a few additional optional values. Those optional values are perfect fit for “opts” but I realized that at first look at the function it is not clear what values are needed and what values are optional. Of course that can be noted in doc-string / comment but I’m thinking it makes sense to make it clear from the implementation.
My rule of thumb:
1. Just use a map when I require firstsecondthirdfourth but they’re too many of them to be positional.
(defn foobar1 [{:keys [first second third fourth]}] ...
(defn foobar2 [{new-count :count new-data :data} something] ...
; do not add defaults - this makes more sense for "opts"
; (defn foobar3 [something {:keys [first second third fourth] :or {fourth 123}}] ...
2. Use opts when values are not needed / defaults are specified or when they’re multiple combinations which prevents use of positional parameters: (firstsecond, firstthird …).
I know a lot of people will say “just use a map” but if your codebase is going to require Clojure 1.11, I would say to write the functions to take named arguments since then you have the option of calling them with either named arguments or a single hash map or a combination. So I would say “always do #2”.
At work, we’re on 1.11 (in production) and so we can always write functions to take named arguments, regardless of how we end up calling them.
In other words, always declare as & {:keys [..]} or & {:as opts} and then call it however is most readable/convenient.
It’s worth noting that {:keys [first second third fourth]} does not actually require all four to be present, it just declares what you want automatically bound within the function.
If your codebase has to live in the Clojure 1.10 world, then I’d say “just use a map” as the last parameter, i.e., #1foobar1 above (I would say never do foobar2 – always have the parameter map in the last slot). If you do that and all your calling code passes a hash map as the last argument, then you can add & to the declaration to switch to #2 if you ever switch to 1.11+ only and you won’t need to update any calling code, but you’ll then have the option when writing new code of using whatever calling sequence is more convenient.
I wanted to avoid this term since in other programming languages it is an official feature, in Cloujure it’s more like an coincidence given by an official feature “destructuring”.
Absolutely, it’s just destructuring. What I meant by requiring is that I want those values to be specified but I know that Clojure won’t stop someone not specifying or passing nil.
OK, makes sense. So I can write it like this:
(defn foobar
"does foo or bar
* named args:
* REQ: first
* REQ: second
* REQ: third
* OPT: color
* OPT: have-fun
"
[foo & {:keys [first
second
third
color
have-fun]
:or {color :red}}]
(if have-fun
(str foo first second third color)
(str foo first second third)))
We haven’t felt the need to do anything special for the first bullet, to be honest.
For the second bullet, & {:keys [stuff i use] :as opts} and then passing on opts as the map is reasonable (although you need to be careful that :or defaults are only added for the local bindings of those keys and are not added to the :as opts binding!).