Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why are many Clojure functions variadic?

Here's an issue I keep running into in Clojure:

user=> (max [3 4 5 6 7])
[3 4 5 6 7] ; expected '7'

Some functions don't do what I expect!
Here's one solution using apply:

user=> (apply max [3 4 5 6 7])
7

Other examples are concat, and min.
My question, as a Clojure newbie, is why are these functions variadic? I expected them to operate on sequences. Is using apply the best/idiomatic way to get what I want?


Note: I'm not trying to say that it's bad to have variadic functions, or that there is a better way. I just want to know if there's a rule or convention being followed, or if there are specific advantages to such an approach that I should be aware of.


Edit: I think the original question was unclear. Here's what I meant:

In other programming languages I've used, there are monoid-like operations, such as adding numbers, finding the greater element, concatenating lists.

There are often two use cases for these operations:

1) combining two elements, using a function that accepts two arguments
2) combining 0 to n elements, using a function that accepts a list (or sequence) of elements

A function for the second case can be built from that for the first case (often using reduce).

However, Clojure adds a third use case:

3) combining 0 to n elements, using a variadic function

So the question is, why does Clojure add this third case?
Paul's answer indicates that:

  • this allows code that is more flexible
  • there are historical forces at work
like image 951
Matt Fenwick Avatar asked Oct 19 '11 15:10

Matt Fenwick


2 Answers

1) Convenience. With math functions such as + it would be annoying to wrap everything in sequences when you just try to do some calculations.

2) Efficiency. Wrapping everything with collections/sequences would also be inefficient, first the sequence needs to be created and then it needs to unpacked at runtime instead of looking up the right Java function at compile time.

3) Expectations. This is how these functions work in other Lisps and similarly, with a somewhat different syntax, in other functional languages, so it is a reasonable expectation for people coming to Clojure. I would say that the idiomatic way in other functional languages to apply a function such as + to a sequence would be to use either reduce or foldl/foldr, so this is also in line with how Clojure handles it.

4) Flexibility. The fact that these functions may be used with higher order functions, such as map makes them more convenient to use if they are variadic. Say you have three vectors and you want to apply a function to the elements at the same position. If your function is variadic then you can just use map with multiple collections (map also has to be variadic then ;) ):

(map + [1 2 3 4] [2 3 4 5] [3 4 5 6])
; [6 9 12 15]

This is a lot more convenient than what you would have if all those functions just took collections.

Idiomatic use: (Edited after kotarak's excellent comment)

It depends on the function if you should use reduce or apply.

For math functions (+,-,*,/,etc.) that take 2 arguments in the Java world reduce makes more sense as it can directly use the 2 argument Java version. With apply they kind of do an implicit reduce (the function adds two arguments and then recurs with the result, the next argument and the rest. That's pretty much what reduce does.)

For str using apply is likely more efficient. When str is called with more than one argument it creates a StringBuilder, adds all the arguments to it and then creates a string. Using reduce the StringBuilder will be created n-1 times and only add one string each time. This is as in the Shlemiel the painter joke, resulting in O(n^2) complexity.

Verdict so far: Using apply with math functions doesn't hurt much, but using reduce with str may be pretty expensive.

like image 112
Paul Avatar answered Oct 14 '22 04:10

Paul


Alex Miller just wrote about a related issue in his blog: 2 is a smell

like image 39
Julien Chastang Avatar answered Oct 14 '22 02:10

Julien Chastang