Search code examples
clojure

What is the benefit of defining the "or" operation in clojure as a macro rather than simply as a function?


I'm hoping the explanation will give me better insight into the advantages of using macros.


Solution

  • In a function, all arguments are evaluated before its invocation.

    This means that or as a function cannot be lazy, whereas a macro can rewrite or into an if statement that only evaluates branches when it's necessary to do so.


    A bit more concretely, consider:

    (or (cached-lookup) (expensive-operation))
    

    ...what it gets rewritten into looks like:

    (let [or__1234__auto (cached-lookup)]
      (if or__1234__auto
        or__1234__auto
        (expensive-operation)))
    

    ...such that we only evaluate (expensive-operation) if the return value of (cached-lookup) is nil or false. You couldn't do that with a function while implementing regular JVM calling conventions: expensive-operation would always be evaluated, whether or not its result is needed, so that its result could be passed as an argument to the function.


    Incidentally, you can implement a function in this case if you take zero-argument functions as your arguments. That is to say, you can do this:

    (defn or*
      ([] false)                                                ; 0-arg case
      ([func-one] (func-one))                                   ; 1-arg case
      ([func-one func-two]                                      ; optimized two-arg case
       (let [first-result (func-one)]
         (if first-result
           first-result
           (func-two))))
      ([func-one func-two & rest]                               ; general case
       (let [first-result (func-one)]
         (if first-result
           first-result
           (apply or* func-two rest)))))
    

    When you must implement a macro, it's often helpful to have it generate "thunks" (anonymous functions), and pass them to higher-order functions such as this one; this substantially aids composability, as a function can be wrapped, modified, or called using higher-level functions such as partial in ways a macro cannot.