Want this course?
Writing our first macro
Course: Beginning with Clojure Macros
We jump right in and start coding a simple macro, discovering the sigils we need along the way.
Code is available: lispcast/macro-playground
You can checkout the code in your local repo with this command:
$CMD git clone https://github.com/lispcast/macro-playground.git $CMD cd macro-playground
Let's write a macro. But first, on our way to writing that macro, we're going to start with a function. So let's start a function called square that takes an x and it returns x times x. And we can see, we could do square ten, we get 100, and we can even square a random integer, that's seven times seven, 49. Okay, nice.
So now, we know that a macro is a function from code to code, so instead of returning the number 100 or the number 49, let's return the code that that would return. So we know that with parentheses, this code is parentheses, right, which means a list, so we do list, and then that first thing there, that asterisk there it is, multiplication is a symbol, and then we want to put in the x of x. So that's our square, and we see we get the list with times ten and ten.
So we made a function that returns code, okay. So the next step is actually just to turn this into a macro, there's a macro called defmacro, that tells the compiler that this thing runs at compile time. So let's say that and then when we do square ten this time, we get 100 again, because the macro expansion the compiler happens, and then it gets passed to the runtime which evaluates it at 100.
And there's a cool thing we could do, you can actually see the macro expansion by passing in the code. So you quote the expression, the expression is square 10, and that turns it into a quoted list with square and 10, and then we see how it's macro expanded. And so we can see that.
So this a macro, it returns a list of star x x, but it's kind of hard to read. What does this code eventually look like? Well you can imagine it, you can run the macro in your head, and you can say "Oh I see, it's gonna look like the code times ten ten." Because that's a pain to do in your head, there's a convenience syntax for making your code look way more alike what it's gonna look like. And that is the backtick. So this is one of the first magic sigils that are useful in macros. It's useful elsewhere, but it was specifically made for macros. So if we put this sigil here right in front, it's called a backtick or a backquote, this is going to now expand like this.
Okay, so there's two things that I notice. The first thing is that wow, everything has name spaces on it, right, that's something that the backquote operator does. And then the other thing is, instead of 10, we now have the x in there.
X has been put in both places, and that's not really the code we want because what is x? It doesn't exist. For this code here, x is not defined. If we try to run it, we'll see that. No such var x.
Okay, so that's expected. The thing that's happening is we are confusing runtime and compile time. This is the code that will run at runtime, which is times x x, but we really want the x to be evaluated at compile time because I'm passing this 10 in, and we want that 10 to be passed in, not the x, right?
So let's use another sigil, it's the tilde, it's also called unquote, that's the operation it does, and what this does is it says, "run this at compile time". So this is saying, this backquote says, "this is for runtime", and then this is saying, "ah except here's a hole where it should be at compile time". Okay and so by alternating a backtick and an unquote, you are able to, the backtick is also called syntax quote, so you do syntax quote and unquote, syntax quote unquote, you're able to say exactly what is happening at runtime and what is happening at compile time.
And this takes practice, but it's a simple concept. So now when I do a macro expand, boom the ten, which is evaluated at compile time, gets put into this expression which is then gonna be passed onto runtime. And we can see, when I call it with ten we get the right answer.
Okay, but this now has a new problem. Let's say I go back to one of the original examples rand-int 10. I'm calling it but these don't really look like squares to me, well that one does, but not all of them do. No that's not a square, see these aren't squares. So what's happening? Well, we're gonna use our macro expand function. This is very common when you're doing macros, gonna see what it looks like, and we see that this code for rand-int got put in twice, and so each time it's generating a different random number and passing it to times.
That's not really what we want, so we still haven't gotten to where we want to be with this. So if I did want to do that let's say in code, not in a function but just in code, what would it look like? So really I want to generate one random number, save it to a variable, and then call times on it. So it would look like this. Let x rand-int 10 and then times x x right?
And that would give me the number I want. So we get to make our macro look like this. That's pretty simple. So we need to put a let and we need to put some variable name, we can call it x, and then we're gonna backquote x here, and now we want this to run just like this one, so we need to have x and x.
Alright, let's see if this works. We're gonna macroexpand it, and it looks like we are looking at that syntax quote, it's expanding this x to have a namespace, and so I don't know if that will work, I bet it won't. It can't let a qualified name. So that's saying that this is illegal in this position in a let, that you can't have a symbol with a namespace in that position. So we need a way to make this not have a namespace, and Clojure of course, has a way.
When you're in syntax quote, if you put a pound sign after the variable, it will make a new variable for you. And so then you need that same pound sign everywhere, and let's see what this looks like. So this is what's happening, it's generating this new symbol, notice it doesn't have a namespace and it's this crazy name that's unique because it's got this number in there that keeps incrementing. It's also got this auto, and you would probably never write your own variable name like this so it won't ever conflict, and so then this gets passed on with the same name, see it's got the same thing, so closure keeps track of all these x's with the pound sign after, they expand to the same variable. Okay so now let's test our square with a random-int. Oh did I say read-int? Yeah I meant rand-int.
Okay, so now we're seeing squares, so it appears to be working and the code looks right. Now what happened here is another question that could come up. Why is this now a let star? I wrote let. Well let is actually a macro and it expands into let star, and what macroexpand does is it keeps expanding this outer form until there's no macros left. If you don't want to expand it all the way, you can just do one expansion, call it macroexpand-1, and now you see that it's just one let, right, and it still will expand the namespace because it's inside that syntax quote, but it's just one level of macro expansion, which sometimes is exactly what you want.You don't want to expand all the internal code, internal macros that come with Clojure. And we're gonna see that in the next lesson.