Variadic function vs just work with collection

Hi Everybody,

Could you please tell me is there a rule of thumb for creating functions with varying number of arguments? Like this:

(defn fun-with-fn [& args]
  (str "I have seqable " (seqable? args) ": " args))

and then using it

(fun-with-fn 1 2 3)
;; => "I have seqable true: (1 2 3)"

or

(apply fun-with-fn [1 2 3])
;; => "I have seqable true: (1 2 3)"

or just create a function that works with sequences?

(defn fun-with-fn2 [args]
  (str "I have another seqable " (seqable? args) ": " args))
(fun-with-fn2 [1 2 3])
;; => "I have another seqable true: [1 2 3]"

My understanding is that varying number of arguments is nicer for people since (+ 1 2 3) looks better than (+ [1 2 3]) but for example (clojure.string/join ["a" "b" "c"]) takes sequence instead of (join "a" "b" "c") so I don’t know…

Could you please tell me when should I prefer sequence / when to use varargs?

Thank you.

Matys

2 Likes

I think the single arg makes sense here. Varargs are useful for aesthetic purposes, and they make sense in some cases. However discrete arities will be faster (on jvm at least, probably clr) since they are compiled to specific class constructors based on arg count. Varargs have to be marshalled to a seq every time, incurring some overhead. That’s why many core functions have at least a few common low order arities defined, and then a varargs variant.

I tend to work on collections. The only varargs stuff semi regularly define would be keyword args stuff. Performance considerations aside (may even be moot for most use cases), I think it’s dealer’s choice.

1 Like

Thank you. Another benefit that I realized is that varargs have to have the & args as the last argument and sometime it makes more sense to have it as first or second…

Could you please tell me, do you have a project with the “keyword stuff” somewhere in your github? I’d love to take a look how pros like you use keyword opts (I’m assuming keyword stuff are optional parameter/arguments, aka opts).

Thank you @joinr !

Clojure - Keyword argument functions now also accept maps has a pretty good example of a varargs with a keyword map, as well as an emerging feature improvement that allows you to pass in a map or keyword args. Some folks are strongly opinionated on never using the kwargs form and instead using a concrete map with the stuff in it. This language feature coming in 1.11 basically unifies the concepts.

A heuristic:

If the parameters make sense on their own and could potentially be named, varargs makes sense. If it’s just a bunch of things use a collection argument.

For example, consider the mathematical functions. It would make sense to write for example

(* mass (/ delta-velocity delta-time))

rather than

(def division-args [delta-velocity delta-time])
(def acceleration (/ division-args)
(def multiplication-args [mass acceleration])
(def force (* multiplication-args)

For me, at least it doesn’t make sense to group delta-velocity and delta-time together in a collection. I’m deffing all the intermediary steps here to make a point about the relationships of all these different values here.

Varargs should be used sparingly as it doesn’t compose as nicely with other functions. If in doubt, I would use a collection.

Var-args are easier to use when the number of arguments you’ll be calling the function with is going to often be hard-coded by the programmer. If not collections are better.

For example, take +, in general when you use + you hard code the number of arguments:

(+ a b)

As you’d tend to use it within a known formula, or a known algorithm.

But say you were doing a calculator, and now the user could type any number of things to add together, so you don’t know how many, and thus you can’t write:

;; Maybe the user wants to add 4 numbers
;; so you can't statically call it with only 3
(+ a b c)

So in this case, you’d be better off with it taking a collection so you could do:

(+ things-from-users)

Now at the end of the day, there’s ways to wrangle each one, so one can use apply in this case. So it’s a matter of what’s the most likely use case for it?

For + I think most likely you know how many arguments when you call it. You might call it with different number of arguments in different places, so vararg still makes sense, but each place you call it you’ll know with how many you need to call it with. So a vararg is good, otherwise it be annoying to always have to wrap it in an extra coll:

(+ [a b])

So I guess a good heuristic is:

Will the caller most often need to pass in a non-deterministic number of arguments to my function at its call site?

If yes, take a collection, else take a var-arg.

And if the different callers amongst themselves won’t need to call it with different number of arguments, then you need neither vararg nor take in a collection. Same if they only will need to call with a very few set of different number of args, then you might be better with overloading arities.

1 Like

One of the reasons I asked was my confusion around clojure.string/join that seems like it’s breaking the norm since I more often hardcode (join [a b c]) rather than joining some runtime inputs (join a-lot-of-things).

And then I realized my mistake. Clojure is gently nudging me to use (str a b c) for hardcoded stuff and (join a-lot-of-things) for runtime stuff. Thanks @didibus :wink:

2 Likes