Writing a Control Flow Macro

Clojure provides the if special form as part of the language:

 (​if​ (= 1 1) (println ​"yep, math still works today"​))
 | yep, math still works today

Some languages have an unless, which is (almost) the opposite of ifunless performs a test and then executes its body only if the test is logically false.

Clojure doesn’t have unless, but it does have an equivalent macro called when-not. For the sake of having a simple example to start with, let’s pretend that when-not doesn’t exist and create an implementation of unless. To follow the rules of Macro Club, begin by trying to write unless as a function:

 ; This is doomed to fail...
 (​defn​ unless [expr form]
  (​if​ expr nil form))

Check that unless correctly evaluates its form when its test expr is false:

 (unless false (println ​"this should print"​))
 | this should print

Things look fine so far. But let’s be diligent and test the true case, too:

 (unless true (println ​"this should not print"​))
 | this should not print

Clearly something has gone wrong. The problem is that Clojure evaluates all the arguments before passing them to a function, so the println is called before unless ever sees it. In fact, both calls to unless earlier call println too soon, before entering the unless function. To see this, add a println inside unless:

 (​defn​ unless [expr form]
  (println ​"About to test..."​)
  (​if​ expr nil form))

Now you can clearly see that function arguments are always evaluated before they are passed to unless:

 (unless false (println ​"this should print"​))
 | this should print
 | About to test...
 
 (unless true (println ​"this should not print"​))
 | this should not print
 | About to test...

Macros solve this problem, because they don’t evaluate their arguments immediately. Instead, you get to choose when (and if!) the arguments to a macro are evaluated.

When Clojure encounters a macro, it processes it in two steps. First, it expands (executes) the macro and substitutes the result back into the program. This is called macro expansion time. Then, it continues with the normal compile time.

To write unless, you need to write Clojure code to perform the following translation at macro expansion time:

 (unless expr form) -> (​if​ expr nil form)

Then, you need to tell Clojure that your code is a macro by using defmacro, which looks almost like defn:

 (​defmacro​ name doc-string? attr-map? [params*] body)

Because Clojure code is just Clojure data, you already have all the tools you need to write unless. Write the unless macro using list to build the if expression:

 (​defmacro​ unless [expr form]
  (list ​'if​ expr nil form))

The body of unless executes at macro expansion time, producing an if form for compilation. If you enter this expression at the REPL:

 (unless false (println ​"this should print"​))

then Clojure will (invisibly to you) expand the unless form into the following:

 (​if​ false nil (println ​"this should print"​))

Then, Clojure compiles and executes the expanded if form. Verify that unless works correctly for both true and false:

 (unless false (println ​"this should print"​))
 | this should print
 -> nil
 
 (unless true (println ​"this should not print"​))
 -> nil

Congratulations, you have written your first macro. unless may seem pretty simple, but consider this: what you have just done is impossible in most languages. In languages without macros, special forms get in the way.

Special Forms, Design Patterns, and Macros

Clojure has no special syntax for code. Code is composed of data structures. This is true for normal functions but also for special forms and macros.

Consider a language with more syntactic variety, such as Java. In Java, the most flexible mechanism for writing code is the instance method. Imagine that you’re writing a Java program. If you discover a recurring pattern in some instance methods, you have the entire Java language at your disposal to encapsulate that recurring pattern.

Good so far. But Java also has lots of “special forms” (although they’re not normally called by that name). Unlike Clojure special forms, which are just Clojure data, each Java special form has its own syntax. For example, if is a special form in Java. If you discover a recurring pattern of usage involving if, there’s no way to encapsulate that pattern. You can’t create an unless, so you’re stuck simulating unless with an idiomatic usage of if:

 if​ (!something) ...

This may seem like a relatively minor problem. Java programmers can certainly learn to mentally make the translation from if (!foo) to unless (foo). But the problem is not just with if: every distinct syntactic form in the language inhibits your ability to encapsulate recurring patterns involving that form.

As another example, Java new is a special form. Polymorphism is not available for new, so you must simulate polymorphism, for example with an idiomatic usage of a class method:

 Widget w = WidgetFactory.makeWidget(...)

This idiom is a little bulkier. It introduces a whole new class, WidgetFactory. This class is meaningless in the problem domain and exists only to work around the constructor special form. Unlike the unless idiom, the “polymorphic instantiation” idiom is complicated enough that there’s more than one way to implement a solution. Thus, the idiom should more properly be called a design pattern.

Wikipedia defines a design pattern[40] to be a “general reusable solution to a commonly occurring problem in software design.” It goes on to state that a “design pattern is not a finished design that can be transformed directly (emphasis added) into code.”

That’s where macros fit in. Macros provide a layer of indirection so that you can automate the common parts of any recurring pattern. Macros and code-as-data work together, enabling you to reprogram your language on the fly to encapsulate patterns.

Of course, this argument doesn’t go entirely in one direction. Many people would argue that having a bunch of special syntactic forms makes a programming language easier to learn or read. We do not agree, but even if we did, we’d be willing to trade syntactic variety for a powerful macro system. Once you get used to code as data, the ability to automate design patterns is a huge payoff.

Expanding Macros

When you created the unless macro, you quoted the symbol if:

 (​defmacro​ unless [expr form]
  (list ​'if​ expr nil form))

But you didn’t quote any other symbols. To understand why, you need to think carefully about what happens at macro expansion time:

  • By quoting if, you prevent Clojure from evaluating if at macro expansion time. Instead, evaluation strips off the quote, leaving if to be compiled.

  • You don’t want to quote expr and form, because they’re macro arguments. Clojure will substitute them without evaluation at macro expansion time.

  • You don’t need to quote nil, since nil evaluates to itself.

Thinking about what needs to be quoted can get complicated quickly. Fortunately, you don’t have to do this work in your head. Clojure includes diagnostic functions so that you can test macro expansions at the REPL.

The function macroexpand-1 shows you what happens at macro expansion time:

 (macroexpand-1 form)

Use macroexpand-1 to prove that unless expands to a sensible if expression:

 (macroexpand-1 '(unless false (println ​"this should print"​)))
 -> (​if​ false nil (println ​"this should print"​))

Macros are complicated beasts, and we cannot overstate the importance of testing them with macroexpand-1. Let’s go back and try some incorrect versions of unless. Here’s one that incorrectly quotes the expr:

 (​defmacro​ bad-unless [expr form]
  (list ​'if​ ​'expr​ nil form))

When you expand bad-unless, you’ll see that it generates the symbol expr, instead of the actual test expression:

 (macroexpand-1 '(bad-unless false (println ​"this should print"​)))
 -> (​if​ expr nil (println ​"this should print"​))

If you try to actually use the bad-unless macro, Clojure will complain that it can’t resolve the symbol expr:

 (bad-unless false (println ​"this should print"​))
 -> java.lang.Exception​:​ Unable to resolve symbol​:​ expr in this context

Sometimes macros expand into other macros. When this happens, Clojure will continue to expand all macros, until only normal code remains. For example, the .. macro expands recursively, producing a dot operator call, wrapped in another .. to handle any arguments that remain. You can see this with the following macro expansion:

 (macroexpand-1 '(.. arm getHand getFinger))
 -> (clojure.core/.. (. arm getHand) getFinger)

If you want to see .. expanded all the way, use macroexpand:

 (macroexpand form)

If you macroexpand a call to .., it will recursively expand until only dot operators remain:

 (macroexpand '(.. arm getHand getFinger))
 -> (. (. arm getHand) getFinger)

(It’s not a problem that arm, getHand, and getFinger don’t exist. You’re only expanding them, not attempting to compile and execute them.)

Another recursive macro is and. If you call and with more than two arguments, it will expand to include another call to and, with one less argument:

 (macroexpand '(and 1 2 3))
 -> (let* [and__3585__auto__ 1]
  (​if​ and__3585__auto__ (clojure.core/and 2 3)
  and__3585__auto__))

This time, macroexpand does not expand all the way. macroexpand works only against the top level of the form you give it. Since the expansion of and creates a new and nested inside the form, macroexpand does not expand it.

when and when-not

Your unless macro could be improved slightly to execute multiple forms, avoiding this error:

 (unless false (println ​"this"​) (println ​"and also this"​))
 -> java.lang.IllegalArgumentException​:​ ​
 Wrong number of args passed to​:​ macros$unless

Think about how you would write the improved unless. You’d need to capture a variable argument list and stick a do in front of it so that every form executes. Clojure provides exactly this behavior in its when and when-not macros:

 (when test & body)
 (when-not test & body)

when-not is the improved unless you’re looking for:

 (when-not false (println ​"this"​) (println ​"and also this"​))
 | this
 | and also this
 -> nil

Given your practice writing unless, you should now have no trouble reading the source for when-not:

 ; from Clojure core
 (​defmacro​ when-not [test & body]
  (list ​'if​ test nil (cons ​'do​ body)))

And, of course, you can use macroexpand-1 to see how when-not works:

 (macroexpand-1 '(when-not false (print ​"1"​) (print ​"2"​)))
 -> (​if​ false nil (do (print ​"1"​) (print ​"2"​)))

when is the opposite of when-not and executes its forms only when its test is true. Note that when differs from if in two ways:

  • if allows an else clause, and when does not. This reflects English usage, because nobody says “when … else.”

  • Since when does not have to use its second argument as an else clause, it’s free to take a variable argument list and execute all the arguments inside a do.

You don’t really need an unless macro. Just use Clojure’s when-not. Always check to see whether somebody else has written the macro you need.

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

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