Using "opts" vs. just a map - proper Clojure style

Hi Everybody,

I have more a philosophical question rather than technical since all I’m going to describe is completely doable, but it might not be the proper way…

I use Clojure 1.11.0-alpha1 and the most current implementation of “opts” described here: Clojure - Keyword argument functions now also accept maps

I use “opts” mainly when I need to pass more than 4 values into a function since in GitHub - bbatsov/clojure-style-guide: A community coding style guide for the Clojure programming language they mention:

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 first second third fourth 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: (first second, first third …).

(defn foobar4 [& {:keys [first second third fourth] :or {fourth 123} :as opts}] ...

(defn foobar5 [something & {new-count :count new-data :data :or {new-count 200}}] ...

3. And possibly even use both when something is required and something is not:

(defn foobar6 [{:keys [first second third fourth] & {:as opts}}] ...

What do you think? Does it make sense? Or maybe rather always use & {:as opts}?

Thank you Clojureverse and thank you Clojureverse people! You’re awesome community. :wink:

Kind regards,

Mark

2 Likes

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., #1 foobar1 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.

5 Likes

Yes, only 1.11+

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)))

Thank you @seancorfield

1 Like

I’m curious if you have followed any conventions (w.r.t. 1.11 named kwargs) to address the following:

  • Easily distinguish between required and optional arguments (when reading fn signature)
  • Intermediary functions that use few options and forward the rest
1 Like

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!).