in clojure.core
there is the apply
function, written in a multiple arity style (presumably for efficiency).
However I ask whether an implementation like thus:
(defmacro apply [f coll] (conj (seq coll) f))
would not be better.
I am aware of the fact that a macro is less efficient than a function, but does it really make such a difference?
And alternatively, is it not possible to write this macro as a function (I've tried but failed, but that's no proof as I have nil experience writing macros)?
The reason apply
is a function instead of a macro is that it has to be. The macro version you've defined is useless: it doesn't work.
But why not? It seems like a reasonable idea. Let's try it out, but call it mapply
so we can still refer to the existing apply
:
=> (defmacro mapply [f coll] (conj (seq coll) f))
I bet you did this, and I bet you even tested it: you can use mapply
in the absolute simplest of cases, like so:
=> (macroexpand '(mapply + [1 2 3]))
(+ 1 2 3)
=> (mapply + [1 2 3])
6
The problem is that this isn't useful: you wrote (mapply + [1 2 3])
, but it would have been just as easy (easier, really) to write (+ 1 2 3)
instead. So this use case is not evidence that mapply
is useful. When else would an apply
-like macro be useful?
The real value of apply
is that you can apply it to lists determined at runtime: when the argument list is fixed at compile time, you don't need apply
because you could just write (f x y)
instead of (apply f [x y])
.
So let's try your version on a list computed at runtime:
=> (apply + (range 4))
6
=> (macroexpand '(mapply + (range 4)))
(+ range 4)
=> (mapply + (range 4))
Execution error (ClassCastException) at user/eval2051 (REPL:1).
clojure.core$range cannot be cast to java.lang.Number
What happened? Your mapply
saw that (range 4)
was a collection, so it put +
at the front of it. What you wanted was to evaluate (range 4)
, yielding a list, and then add its elements somehow. But because mapply
works at compile time, the list it sees is (range 4)
: a two-element list containing the elements range
and 4.
And this isn't just some simple bug in your mapply
definition: it's impossible to define mapply
as a macro (except as a trivial one that does no work at compile time and delegates to apply
at runtime). For a more clearly-impossible example, consider calling your mapply
from inside a function:
=> (defn sum [xs] (apply + xs))
=> (sum (range 4))
6
=> (defn msum [xs] (mapply + xs))
Syntax error macroexpanding mapply at (REPL:1:17).
Don't know how to create ISeq from: clojure.lang.Symbol
Since mapply
is a macro, it of course runs at compile time, i.e. at the time we are defining msum
. It only gets one chance to expand, without knowing what xs
we might pass it in the future. So when it looks at xs
, it sees simply the symbol xs
, not some list. As a result the seq
call inevitably fails. And again, no other implementation would work, because you simply don't know at compile time how many elements the list has, and can't expand to the right call.
As a final treat, consider another reason apply
couldn't be a macro: you can apply a function to an infinite list of arguments, but if you try to generate an infinitely long macro expansion the compiler will have a very sad time.
=> (defn apply-to-nats [f] (apply f (range)))
=> (apply-to-nats (fn [& args] (first args)))
0
=> (defmacro mapply-to-nats [f] (cons f (range)))
=> (mapply-to-nats (fn [& args] (first args)))
Syntax error (OutOfMemoryError) compiling at (REPL:1:1).
GC overhead limit exceeded
What happened? The compiler ran in a loop until my system ran out of memory, trying to compile
((fn [& args] (first args)) 0 1 2 3 4 ...)