I'm hoping the explanation will give me better insight into the advantages of using macros.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With