Let's take a look at the text for problem 4 (emphasis added):
Write a macro log
which uses a var, logging-enabled
, to determine whether or not to print an expression to the console at compile time. If logging-enabled
is false, (log :hi)
should macroexpand to nil
. If logging-enabled
is true, (log :hi)
should macroexpand to (prn :hi)
. Why would you want to do this check during compilation, instead of when running the program? What might you lose?
Your macro conforms to this specification. We can confirm this with the following experiments:
;; With logging-enabled false
(def logging-enabled false)
(macroexpand '(log :hi))
;;=> nil
;; With logging-enabled true
(def logging-enabled true)
(macroexpand '(log :hi))
;;=> (clojure.core/prn :hi)
Note what we're passing to macroexpand
: '(log :hi)
. '
is a reader shortcut for quote
. So this is equivalent to (quote (log :hi))
:
(read-string "'(log :hi)")
;;=> (quote (log :hi))
This is important, because macroexpand
is a function, so its argument is evaluated before macroexpand
is called.
What might you lose?
I think this is a good example of doing too much at compile time. By performing the check at compile time, you save a little bit of time by not performing the check at runtime. But, you lose the ability to turn logging on and off dynamically -- whatever logging-enabled
was set to when a particular log
form was macroexpand
ed determines whether that form will print a message or not.
It would be more practical if this check is performed at runtime. Then we can enabled and disable logging at runtime.
While we're at it, we may as well make logging-enabled
a dynamic var. That way, we can strategically enable / disable logging within specific dynamic scopes.
(def ^:dynamic *logging-enabled* false)
By convention, dynamic vars have their names augmented with *
s. This isn't a requirement, but it does serve as a reminder that the var is dynamic.
Now, we can define log
as:
(defmacro log [expr]
`(when *logging-enabled*
(prn ~expr)))
`
indicates quasi-/syntax-quotation. Basically, it's similar to quote
, except you can escape the quotation using ~
.
With this definition of log
, there is a minute amount of runtime overhead for evaluating a log
form -- lookup of the (dynamic) value of *logging-enabled*
, and checking the resulting value. But, if *logging-enabled*
is false
, that's it -- we don't evaluate the expression passed to log
, and we don't print anything to the console. So the runtime overhead is minimal.
We can use this new version of log
like this:
;; Root value for *logging-enabled* is false
(log :hi)
;;=> nil
;; dynamically bind *logging-enabled* to true
(binding [*logging-enabled* true]
(log :hi))
;; Prints ":hi"