Transforming Code

Now you have this sort of abstract idea that you can view code in two different ways: either as code or as data. But what does that mean concretely?

Let’s put this in terms you already know by thinking about the first two phases of the REPL (Read-Eval-Print loop, remember?). The first phase, read, takes a character-based representation (typically an input stream), and turns it into Clojure data structures. So the output of the read phase is data, which is then interpreted by the second phase, eval, as code! Let’s take a closer look so you’ll understand how to transform the code-as-data from the reader into the data-as-code that will be evaluated.

read is a convenient way for us to build the expressions that macros will eventually act on. Let’s look at a few inputs and outputs for read, make sure you follow why things work the way they do, and that’ll give you enough to dive headfirst into your first macro. The actual read function in Clojure consumes streams, which can be a bit verbose to set up. So for the purposes of these examples, we’ll use its sibling read-string, which consumes strings instead.

When we read this expression out of a string, we get back a list:

basics/read_string.clj
 
(​read-string​ ​"(+ 1 2 3 4 5)"​)
 
;=> (+ 1 2 3 4 5)
 
(​class​ (​read-string​ ​"(+ 1 2 3 4 5)"​))
 
;=> clojure.lang.PersistentList

This list is one of the pieces of data we’ve been talking about. It’s a Clojure list, and yet it’s also Clojure code. In the format that comes back from read-string, it’s ready for evaluation. When we eval that list, we get what we’d expect as the value of that expression:

basics/eval_expression.clj
 
(​eval​ (​read-string​ ​"(+ 1 2 3 4 5)"​))
 
;=> 15
 
(​class​ (​eval​ (​read-string​ ​"(+ 1 2 3 4 5)"​)))
 
;=> java.lang.Long
 
(​+​ 1 2 3 4 5)
 
;=> 15

This is pretty much what happens when you write code in a Clojure REPL, or when you run a full-blown Clojure project: the code is read into data structures and then evaluated. Given these two distinct steps, it’s not too hard to imagine wedging ourselves in there between the read and eval steps in order to change the code to be evaluated. For instance, we could replace addition with multiplication:

basics/replace_addition.clj
 
(​let​ [expression (​read-string​ ​"(+ 1 2 3 4 5)"​)]
 
(​cons​ (​read-string​ ​"*"​)
 
(​rest​ expression)))
 
;=> (* 1 2 3 4 5)
 
(​eval​ *1) ​;; *1 holds the result of the previous REPL evaluation
 
;=> 120

And of course, if we eval that new list, we’d get an entirely different result than our original. The code here isn’t bad, but let’s work toward a version that constructs the expression from scratch rather than reading it in from a string, and clean it up a bit in the process. read-string works fine, but it would be much nicer to type them in as lists, without the fuss of going through a string.

But if we want to create the list (+ 1 2 3 4 5), we can’t just type in (+ 1 2 3 4 5), because that would actually evaluate the expression.

basics/replace_addition_broken.clj
 
(​let​ [expression (​+​ 1 2 3 4 5)] ​;; expression is bound to 15
 
(​cons
 
(​read-string​ ​"*"​) ​;; *
 
(​rest​ expression))) ​;; (rest 15)
 
; IllegalArgumentException Don't know how to create ISeq from: java.lang.Long
 
; clojure.lang.RT.seqFrom (RT.java:505)

So we want a different solution, one that suppresses execution somehow. Luckily for us, there’s a verb in Clojure called quote that does exactly that:

basics/replace_addition_2.clj
 
(​let​ [expression (​quote​ (​+​ 1 2 3 4 5))]
 
(​cons​ (​quote​ ​*​)
 
(​rest​ expression)))
 
;=> (* 1 2 3 4 5)

But wait, didn’t I just say that we couldn’t type in (+ 1 2 3 4 5), yet here we are doing it, and seeing the right thing happen anyway! What gives? Note that I said verb instead of function. The quote verb is, in fact, a Clojure special form and not a function. We can think of verbs (not an official language term, just a convenient concept to think about) as the union of functions, macros, and special forms—the things that appear right after the opening parenthesis in the first position of a list. Lists can also appear in the verb position, but they have to reduce down to a function when evaluated. Only functions uniformly evaluate their arguments before passing control to the code that implements the verb. Verbs are summarized in Table 1, Clojure Verbs.


Table 1. Clojure Verbs

VerbHow do we define one?When are the arguments evaluated?
Function

defn

Before executing the body
Macro

defmacro

Depends on macro; possibly multiple times or never.
Special form

We can’t! They’re only defined by the language.

Depends on special form; possibly multiple times or never.

Let’s take a closer look and see how functions evaluate their arguments:

basics/function_argument_evaluation.clj
 
(​defn​ print-with-asterisks [printable-argument]
 
(​print​ ​"*****"​)
 
(​print​ printable-argument)
 
(​println​ ​"*****"​))
 
 
(print-with-asterisks ​"hi"​)
 
; *****hi*****
 
;=> nil

When we just send something like a string into print-with-asterisks, we don’t really need to think about the timing of the argument evaluation. But if we use an expression for the argument, we see that the expression is evaluated before any of the function body is evaluated.

basics/function_argument_evaluation-2.clj
 
(print-with-asterisks
 
(​do​ (​println​ ​"in argument expression"​)
 
"hi"​))
 
; in argument expression
 
; *****hi*****
 
;=> nil

Macros and special forms, on the other hand, are not bound by this rule and may evaluate the arguments in whatever order they define (or do something entirely different!). This is a key difference, perhaps the key difference, between functions and macros, and we’ll look at the consequences of this later on.

None of the code inside the quote expression, often bounded by parentheses or square or curly brackets, is evaluated. I say often instead of always, because you can also quote smaller tokens, things like numbers, strings, keywords, and symbols. We tend to use quote most often on symbols, lists, and (somewhat less often) vectors.

basics/quoting_tokens.clj
 
(​quote​ 1)
 
;=> 1
 
 
(​quote​ ​"hello"​)
 
;=> "hello"
 
 
(​quote​ :kthx)
 
;=> :kthx
 
 
(​quote​ kthx)
 
;=> kthx

There’s a potentially confusing fact here, that things like numbers, strings, and keywords already appear to be themselves when being read. This simplifies things when we actually manipulate these expressions, and the fact is that there aren’t many good reasons why you’d want different behavior here. Symbols and lists are the special ones: symbols are for representing locals, vars, classes, protocols, or other bindings; lists are for invoking verbs.

Believe it or not, quote gets even better, because there’s a shorthand in Clojure called a reader macro, that converts a single quote character to wrap the following expression in the quote form. Reader macros aren’t something you can create yourself—they’re built into the language, and there are only a few of them. So I know it’s hard, but don’t get too excited here!

basics/quote_reader_macro.clj
 
'(​+​ 1 2 3 4 5)
 
;=> (+ 1 2 3 4 5)
 
 
'​map
 
;=> map

The payoff of this exploration of the quoting mechanism is that we can rip out our previous monstrosity (using read-string) and express it much more succinctly with:

basics/replace_addition_3.clj
 
(​let​ [expression '(​+​ 1 2 3 4 5)]
 
(​cons​ '​*​ (​rest​ expression)))
 
;=> (* 1 2 3 4 5)

Now you’re starting to get a clearer idea of what people mean when they say that in Clojure, code is data, and data is code. We’ll get to more complicated usages and variants of quoting soon enough, but this is plenty to get you started writing your first macro.

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

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