Function parameteres

Hello everybody,

I have a couple of questions for you regarding function parameters.

  1. Can anybody explain to me when it’s the best case to pass arguments as maps and when it’s better to not use them? I’m used to passing everything on big maps but sometimes down the road I get lost and I don’t know anymore if the map contains the keys I need. If I pass arguments “normally” I’m at least sure that they have been passed through?

  2. Is there a way to destruct parameters like this but not by making them optional?

(f :a true) → :a get true as value but it’s not optional

1 Like

There are no hard rules or conventions around this.

Some thoughts:

  • Lately the clojure CLI has added support for executing functions from the command line with clojure -X my-ns/my-fn :foo 1 :bar 2 so if you’d like to support that then go with one map argument.
  • It’s fine to put required arguments first to ensure they are there and passing the options last: (foo req1 req2 {:opt1 ...}), an idiom which is well established I’d say.
  • If you’re designing an API, it might be best to go with a consistent style: choose one or the other
  • Both the single map or required args first approaches don’t ensure that the correct things are passed: you can still make typos in a map key or pass the arguments in the wrong positions. To really have some guarantees around this you’ll have to make some assertions. You can do this via:
    • clojure.spec s/fdef or s/assert
    • clojure.core/assert
    • other libraries like Schema / plumbing (defnk), malli, …
      Usually these libraries have some kind of toggle to disable the assertions in production, so you will also have to think about if these assertions are essential in production or OK to use only in development.
4 Likes

In my opinion you shouldn’t pass arguments as maps unless they are options, and therefore optional.

That said, an argument could be a map modeling some domain entity inside your application, or some context state required to be injected into the function.

For example:

(defn move
  [player x y & {:keys [speed]}]
  ...)

Here player can be a map that models your player, so it could look like:

{:name "CoolProRacer2343"
 :x 234
 :y 422
 :score 23
 :turbo true}

But x and y are passed as arguments, though you could also model them as a map of coordinates {:x .. :y ..} if you wanted as well.

Finally, only options are passed at the end on an options map, or as named arguments.

The only time I break away from this model is when the function needs to take a lot of parameters, 5 or more and I might consider doing something more like:

(defn move
  [& {:keys [a b c d e f g]}]
  ...)

;; Let's pretend a b c d e f and g are all parameters that for some reason the move function would need.
;; Having them not named when calling the function would get very confusing that you get their order wrong and can't follow what is what,
;; which is why when there are too many parameters and I find it gets confusing I switch to just
;; one big map of arguments.

It’s unfortunate Clojure doesn’t have a way to have named arguments that are resolved at compile time, but I guess a macro might be able to create something similar.

Not in a way that is a compile time check, but you can use Spec or pre/post conditions to do so as runtime checks.