Macros Can Be Contagious

Next we’ll look at an important consequence of macros not being values: macros that take a variable number of arguments can infect their callers, forcing the author to write more macros instead of functions. Let’s take a look at an example and think about the implications for calling code:

beware/contagious_1.clj
 
(​require​ '[clojure.string :as string])
 
(​defmacro​ log [& args]
 
`(​println​ (​str​ ​"[INFO] "​ (string/join ​" : "​ ~(​vec​ args)))))
 
 
user=> (log ​"that went well"​)
 
;[INFO] that went well
 
;=> nil
 
user=> (log ​"item #1 created"​ ​"by user #42"​)
 
; [INFO] item #1 created : by user #42
 
;=> nil

If you recall what you learned in Chapter 2, Advance Your Macro Techniques, you’ll notice that we’re converting args to a vector instead of just using ~args. Why? Because we want args to be a sequential thing in the macroexpanded code, but not an expression to be evaluated. If we didn’t use a vector, Clojure would stick the args sequence (a list, for all intents and purposes) right into the macroexpanded code and treat the first sequence element as the verb at runtime. As you can imagine, that would go badly if we tried to use a string as the first argument. This macro works fine and is easy to call directly, but suppose we find ourselves holding onto some arbitrary collection of messages, perhaps via input to another function:

beware/contagious_2.clj
 
(​defn​ send-email [user messages]
 
(Thread/sleep 1000)) ​;; this would send email in a real implementation
 
 
(​def​ admin-user ​"[email protected]"​)
 
(​def​ current-user ​"[email protected]"​)
 
 
(​defn​ notify-everyone [messages]
 
(​apply​ log messages)
 
(send-email admin-user messages)
 
(send-email current-user messages))
 
 
; CompilerException java.lang.RuntimeException:
 
; Can't take value of a macro: #'user/log, compiling:(NO_SOURCE_PATH:2:3)

We might have seen this one coming: anytime you see a macro name appear anywhere except the first position in a list, warning bells should go off in your head, since we can’t treat macros as values. But how can we solve this issue? If we knew that there were exactly two messages, or three messages, etc., we might be able to pull specific elements from the input sequence to pass to the macro.

Take a few minutes to write another macro for this use case, assuming that you can’t change the log macro or create a replacement.

Perhaps you came up with something like this:

beware/contagious_3.clj
 
(​defmacro​ notify-everyone [messages]
 
`(​do
 
(send-email admin-user ~messages)
 
(send-email current-user ~messages)
 
(log ~@messages)))
 
 
user=> (notify-everyone [​"item #1 processed"​ ​"by worker #72"​])
 
;[INFO] item #1 processed : by worker #72
 
;=> nil

Note that we need to wrap the three expressions in a do, since a macroexpansion can only return one expression. If we left that out, we’d lose those first two expressions in the macroexpanded code. This macro implementation does work. But of course now we can’t use notify-everyone as a value, so we impose the same restrictions on users of our new code. Macros appear to be taking over our code!

In reality, the far better solution here is one where log isn’t a macro at all. We don’t need a macro here:

beware/contagious_4.clj
 
(​require​ '[clojure.string :as string])
 
(​defn​ log [& args]
 
(​println​ (​str​ ​"[INFO] "​ (string/join ​" : "​ args))))
 
 
user=> (log ​"hi"​ ​"there"​)
 
;[INFO] hi : there
 
;=> nil
 
 
user=> (​apply​ log [​"hi"​ ​"there"​])
 
;[INFO] hi : there
 
;=> nil

Of course, sometimes there are cases where we really do want a macro with varargs, or where it’s not so obvious that there’s an easy way to use a function instead. And in those cases, we really want to be sure that they’re worth the cost of forcing clients to use macros as well.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.198.94