Search code examples
performanceclojuremacrosapply

macro form of clojure.core apply function


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)?


Solution

  • 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 ...)