When should I destructure?

Does anyone have some good guidelines for when to use de-structuring in function arguments and when to use parameters?

I’ve been working on a little app and suddenly I realize that my functions are a bit of a mishmash of destructuring and parameters.

Sometimes it’s like

(defn get-current-line
  "Get the currently selected line object"
  [{:keys [mixers
           current-mixer
           current-line]}]
...)

and sometimes code looks like this

(defn plot-points
  ""
  [points width height]

Reading the official pages about it I get the sense that you should basically always favor destructuring b/c it makes the code simpler: https://clojure.org/guides/destructuring

But then when I look at “core” functions like map, reduce, switch!, basically none of them use destructuring for their interface. Clojure is generally very opinionated, so I’m a bit lost with these different options suddenly :slight_smile:

My initial thoughts are… for private (defn- functions destructuring makes total sense b/c you can know it will conveniently match the data input layout. For more “interface”/API/generic style functionality maybe it’s too much mental overhead to expect the user to pack the input into a correct map?

There is also the issue that destructuring will give you nil for missing parameters while parameters will throw an error.

Hope some gurus can weigh in with their thoughts and help me cook up a better mental model

1 Like
(parse-json [string & {:keys [key-fn] :as opts}])

(handle-login [{:keys [params session] :as ctx}])

(query-user [db {:keys [select where] :as condition}])

(parse-expression [expr {:keys [line-no file-name] :as env])

I think it’s not about how to make code simpler, it is how to put related things together as one argument.

4 Likes

Remember that destructuring can be applied not only for function arguments but also insidelet [ foo bla ] scope.

I personally like the destructuring feature, it is really amazing one.

From my experience usage, as stated by @ShiTianshu, you can add also default so you avoid nils.

My usage of destructring is mostly when I need to acess 1/2 Level max of datastructure.

In case I have more nested structure, I personally use an helper function , because I feel it more readable. But 1 level datastructure I like it to use as function arguments.

If the data is more complex, I mostly use helper functions inside a let scope.

helper functions are opensource and free :smile:!

1 Like

To me destructuring is almost always preferred. It has lots advantages over using let blocks etc to “dig out” required information from a bigger thing and/or separating required info into positionally separate explicit arguments:

  • The space of accepted inputs is much bigger - it’s e.g. any map that contains the required info even if its hundreds of entries. That means the function is much more tolerant of its inputs (in the Postels Law sense) and client code can change around it more easily.

  • It can reduce it’s requirements more elegantly without affecting its clients (unlike dropping the first arg from a 2 arg function which would require a multi arity cases purely for backwards compatibility)

  • Destructuring expresses the function’s requirements directly at the call site. This is much easier for developers to spot what it really uses than a bigger arg that in later code is pulled apart and only 2 bits are used.

  • If a function takes a big thing as its only arg and only uses 2 bits of it, the big thing has to be given a name. Often this naming is hard to keep realistic, and test cases either serve up too much data or unrealistically small amounts of data into the function. It’s an open set of info - very generic - you may think its a “customer” now but it could be a “customer+orders” later, and the function really doesn’t care. Generic names like “context” don’t really help anyone. So avoid naming it at all and destructure instead focusing on the names of the info you actually need. This is another example of reducing the importance of aggregates/entities/containers/objects and instead elevating the importance of individual information items.

  • For the same reasons destructuring also encourages good naming of those low level information items in and of themselves free of context. Namespaced keywords are great for this. This is good for the longevity of your code and avoiding unnecessary coupling of highly nested data that is context dependent.

  • In the case of taking a big thing and using let etc to dig it out, destructuring is often less code, which means less cognitive load, less bugs etc. This is the least important advantage arguably, but still important. (In the case of dividing required info into separate args it’s more code of course!)

In short if you are doing business information processing pipelines that often change (which is what I do mostly), I would highly recommend destructuring.

The only time I wouldn’t destructure would be if the data structure were so nested that destructuring became ugly and hard to read/debug (which is perhaps a smell in itself). Or where the api/domain was so fixed in scope that its inputs/domain were limited, easy to name and unlikely to change, as in often the case in library code for example.

This is a great list, and it includes some important cases I hadn’t considered. The one about relaxing requirements is very powerful.

One use case that’s come up that I feel a bit dirty about is that destructuring allows for a “pass through” type of function calling. You take a big thing, use just an element or two, and then pass the big thing into further function calls that will use other elements. It makes it very clear what elements are used in the given scope, but it ends up hiding what elements the function requires. You only see the keys needed in the immediate scope but not for functions higher on the call stack. So maybe that’s a point not in favor of destructuring :slight_smile:

Maybe I shouldn’t be writing code like that… but I find it useful to have what is in effect a scoped arg-list

To me it just begs the question, do you really need standard sequential arg lists at all? Certain things like the threading macros rely on it heavily (though you could thread maps as well…), and the standard libs basically never destructure for some reason

It just all feels a bit schizophrenic and like a bit of a language wart.