This is a continuation of Programming Language Study–Lisp (3).
There’s plenty more to learn. Let’s continue right where Lisp 3 ended. Let’s figure out how to use macros. I found this article about macros here: http://www.gigamonkeys.com/book/macros-defining-your-own.html, and I read some of it.
I have Emacs open, and as usual it did something I didn’t knowingly ask it to do—it split the window in half so I was viewing two different buffers and I had no idea how to undo that. Googled that. Of course there is a keystroke sequence for closing a window that you are expected to memorize: C-x 0. Emacs is fun, isn’t it?
Here is my first attempt at defining a
custom-defun macro. It does not work, but let’s see if we can figure out why and fix it:
(defmacro custom-defun (name args definition) (print function-name) (defun 'name 'args 'definition)). This produces the error “value ‘NAME is not of the expected type CCL:FUNCTION-NAME.”. At this point I don’t understand why it’s complaining about a variable type because I thought the whole point of macros is that they don’t evaluate anything—they generate code for use later in evaluating things. It should be just treating
name as a meaningless lexical symbol. I’m not trying to say the computer is wrong. I’m saying evidently I need a better understanding of what “macro” means.
Let’s back up and see if I can define a macro that does anything—something less complicated than this exercise. I came up with the simplest thing I could think of that produces some kind of side effect:
(defmacro hello-world () (format t "hello, world")). This works. After executing that to define
hello-world, I executed
(hello-world) and it printed “hello, world” to the standard output. That is something I could have done with a function, but it’s a start.
Maybe let’s read up on some actual examples of things macros can do that functions cannot do. I may Google around on this topic in a bit, but first I’m going to go back to Peter Seibel’s Practical Common Lisp. I think people undervalue the printed word in our industry. The editing process that published books go through adds value. Ain’t nobody editing what I write, and I think that’s pretty evident. I’ll be reading from Chapter 7, “Macros: Standard Control Constructs”.
You may have noticed I don’t tend to follow any particular order when I study. A person on Twitter said something very much like that here: https://twitter.com/Ardubal/status/1259635381030522881. The haphazard nature of my study technique is just what comes most naturally to me—not something I intentionally decided to do. However, I have succeeded academically and professionally in every topic I’ve ever tried to learn. There has never been any subject I couldn’t master better, faster, and deeper than almost everyone else around me.
I think the skipping around is an important part of that. If you try to make learning into an orderly progression, how do you ever know that you have learned A and B well enough to move on to C? If you have a teacher guiding you, then the teacher can tell you that. It’s part of a teacher’s job to maintain the schedule and make judgment calls about whether the student has learned something well enough to just move on to the next thing. But studying alone calls for a different strategy. I did not design my technique with that in mind, but I think it works very well for exactly that reason. I don’t ever stop and think about whether I have learned the previous topic well enough. I just study whatever seems relevant and interesting from one moment to the next.
That doesn’t mean I never make plans. If I realize that I need a detailed and thorough understanding of some particular topic, then I’ll do that. But flexibility is the keyword here, and if I get bored then I’ll just do something else for a while. If I spent a week studying the syntax and control flow of Common Lisp, I… wouldn’t do that because it would bore me so much. I’ll pick those things up as I go the way people learn any language. That is: I’ll instinctively abstract the rules from the examples I see.
Now, I want to study an example from Practical Common Lisp in chapter 7. The author defines a macro as follows:
(defmacro when (condition &rest body) `(if ,condition (progn ,@body)))
In a previous post, I think I made a guess about how variable argument functions/macros might be defined in Lisp, and based on what I see here I think my guess was incorrect. I discovered by experimentation that the
+ function accepts a variable number of arguments, so I knew that structure existed in the language. Here, I see the keyword “rest”, which indicates “the rest of the arguments”. That is something I could guess without Googling, but I did Google it to see if the ampersand played any special role. I’m reading from here: http://www.gigamonkeys.com/book/functions.html.
As far as I can tell,
&rest is one single symbol, not an ampersand symbol combined with the
rest symbol. In the example above, it indicates that all of the remaining arguments should be collected into the
body list. So at least for now I’m not going to go looking for the meaning of a stand-alone ampersand. It may have no meaning by itself, or maybe it does. But at the moment I don’t need to know.
I’m looking at the author’s example defining a macro and comparing it with my attempt at defining a
custom-defun macro above. My attempt did not use a backtick. I thought that a macro would just replace symbols with other symbols. Evidently it does not do that. An operation that replaces symbols with other symbols could work just fine without any notion of what the symbols mean, but in my attempt above, the interpreter returned an error about “expected type”. That is a semantic error, which means it was trying to interpret—not just moving symbols around.
We studied the backtick in my previous post on Lisp, and we studied an expression very similar to this one:
`(if ,condition (progn ,@body)). To summarize, this expression will evaluate to a list.
if symbol is a built-in operator for control flow. The value in scope of
condition will be interpolated into the list. All of the elements in the list
body will be spliced into a new list with the
progn operator, so they will be evaluated in sequence. We saw that operator before.
The result of that expression starting with the backtick is a list that, if evaluated, would perform some conditional logic based on whether
condition evaluates truthy or not. Thus, in the
defmacro when expression above, it seems plausible that the macro has the following effect: it takes an expression of the form
when (condition &rest body) and it replaces that with the value of that backtick expression above.
Is this doing anything a function could not do? So far I don’t see anything in the
when macro that looks any different from what a Lisp function can do. In the Lisp interpreter I’m using, it looks like “when” is a reserved symbol, so I wrote this instead.
(defun when-it (condition &rest body) `(if ,condition (progn ,@body)))
That function definition evaluates and it defines a
when-it function. I tested it by executing
(when-it (= 1 1) (print "a") (print "b")). This causes “a” and “b” to be printed to the console as side effects—as expected. However, after that I executed this expression:
(when-it (= 1 2) (print "a") (print "b")). The
when-it operator was intended for use in control flow, so if the condition evaluates false, then we do not expect to see the side effects printed. But we do. This also prints “a” and “b” to the console.
I did not pre-plan this example. I think you and I have stumbled onto something that might illustrate pretty well what a macro is and how it differs from a function.
Here’s what I think is happening. If you type
(when-it (= 1 2) (print "a") (print "b")) in a Lisp interpreter, then you are passing three arguments into the
when-it function, but it passes them by value. To figure out what you intend to pass into
when-it, the interpreter evaluates all of those lists. It’s just like an earlier example I wrote. If you type
(+ 4 (* 5 6)) then the interpreter has to evaluate
(* 5 6) before it even knows what to pass into
+ as the second argument.
The process of evaluating those lists causing the side effects to happen. Thus: it is not possible to use a function for control flow in this way. Maybe you could achieve what we want here using a lot of quoting and indirection, but that significantly increases the complexity of the expression. And we don’t want that. We want to create a new, simple control flow syntax.
Usually I try to wrap things up better than this before ending a post, but it’s 11 PM on a Sunday night, and I predict this is going to be an extremely busy week for me at work. I’d rather just post this instead of letting it hang in “draft” limbo.
I’ll come back and continue the thought in another post when I can. I think you can see where we were going. But if not, don’t worry. I’ll be back before too long.