12

McCarthy's Elementary S-functions and predicates were atom, eq, car, cdr, cons

He then went on to add to his basic notation, to enable writing what he called S-functions: quote, cond, lambda, label

On that basis, we'll call these "the LISP primitives" (although I'm open to an argument about type predicates like numberp)

How would you define the defmacro function using only these primitives in the LISP of your choice? (including Scheme and Clojure)

Isaac
  • 15,783
  • 9
  • 53
  • 76
hawkeye
  • 34,745
  • 30
  • 150
  • 304
  • 1
    `Defmacro` is a macro itself, not a function. – Svante Aug 21 '10 at 03:12
  • @Svante Not really: http://github.com/clojure/clojure/blob/b578c69d7480f621841ebcafdfa98e33fcb765f6/src/clj/clojure/core.clj#L370 But it does have some questionably macro-like Java underpinnings. It's kinda a convoluted mess. – Isaac Aug 21 '10 at 04:40
  • @J G your questions are always so much fun! Love the thinking that's involved in answering them :) – Isaac Aug 21 '10 at 04:41
  • @Isaac Hodes: It is defined as a macro in the Common Lisp standard. You have to realize that Clojure, while undeniably lispy, is not too close a relative of Scheme or Common Lisp (it does not even have a real `cons`). The gist is, `defmacro` is a special operator in that its arguments are not evaluated, but it need not be a core special operator. – Svante Aug 21 '10 at 08:44
  • @Svante Didn't realize you were referring to the CL version—pardon the error. I realize Clojure is less Lispy in the regard that, at least for now, a lot of it is implemented in terms of its host languag. That should be changing soon! – Isaac Aug 21 '10 at 14:07
  • 2
    @Issac Hodes: Check again, defmacro in Clojure is a macro – Brian Aug 21 '10 at 14:38
  • Interesting… in the source I linked to, the code begins: `(def defmacro (fn […)`. So, while I admitted there were some macroesque things about the definition due to some Java hijinks, it looks like it's a function, right? Or am I missing something. – Isaac Aug 21 '10 at 15:32
  • 1
    Isaac, you are missing something. Look immediately after the end of the (def ...). – dreish Aug 21 '10 at 16:11
  • I do see the `.setMacro`, I guess we're talking past each other. `defmacro` is defined as a function, but then "Java hijinks" make sure that it is passed the environment and that its args aren't evaluated. I guess that makes it a macro, technically, but it *is* defined as a function. I wasn't being clear, and I guess my phrasing left something to be desired. Thanks for clearing up what we were talking about! – Isaac Aug 21 '10 at 20:14
  • 3
    Every macro in Lisp is just a symbol bound to a lambda with a little flag set somewhere, somehow, that eval checks and that, if set, causes eval to call the lambda at macro expansion time and substitute the form with its return value. If you look at the defmacro macro itself, you can see that all it's doing is rearranging things so you get a def of a var to have a fn as its value, and then a call to .setMacro on that var, just like core.clj is doing on defmacro itself, manually, since it doesn't have defmacro to use to define defmacro yet. – dreish Aug 22 '10 at 01:40
  • 1
    Right, that does make sense—I suppose that does mean I should be considering it to be a macro. I was coming at it from the wrong perspective I suppose! Thanks for getting me on the train :) – Isaac Aug 22 '10 at 14:01
  • Also, for perspective, `fn` is also a macro. So in that sense every function is a macro. Clojure's model is a bit different from the older Lisps, to say the least. (http://github.com/clojure/clojure/blob/master/src/clj/clojure/core.clj#L42) – Isaac Aug 23 '10 at 03:19
  • Maybe I should ask another question then. Something like "Can you write the eval function using only the LISP primitives including a defun and lambda functions but not defmacro", and then an extension question for defmacro. – hawkeye Aug 23 '10 at 09:26
  • Sure: think about McCarthy's `eval` function, and consider how it "destructures" the form you pass it to be evaluated before it's evaluated/executed. You could then check to see that X form being looked at by `eval` is a macro (look in your env map, see that it's a macro somehow), and then treat it accordingly (e.g. let `env` do the re-writing before executing). So you'd essentially be expanding your `eval` function to handle one more case, but you wouldn't need another primitive necessarily. Just some sort of way for `eval` to recognize that it has encountered a macro. – Isaac Aug 23 '10 at 19:25
  • This is another [way to look at macro implementation][1] - wonderfully transparent. [1]: http://stackoverflow.com/questions/3465868/how-to-implement-a-lisp-macro-system/10363040#10363040 – hawkeye Apr 28 '12 at 11:26

2 Answers2

6

The problem with trying to do this on a machine like McCarthy's LISP machine is that there isn't a way to prevent argument evaluation at runtime, and there's no way to change things around at compile time (which is what macros do: they rearrange code before it's compiled, basically).

But that doesn't stop us from rewriting our code at runtime on McCarthy's machine. The trick is to quote the arguments we pass to our "macros", so they don't get evaluated.

As an example, let's look at a function we might want to have; unless. Our theoretical function takes two arguments, p and q, and returns q unless p is true. If p is true, then return nil.

Some examples (in Clojure's syntax, but that doesn't change anything):

(unless (= "apples" "oranges") "bacon")
=> "bacon"

(unless (= "pears" "pears") "bacon")
=> nil

So at first we might want to write unless as a function:

(defn unless [p q]
    (cond p nil
          true q))

And this seems to work just fine:

(unless true 6)
=> nil

(unless false 6)
=> 6

And with McCarthy's LISP, it would work just fine. The problem is that we don't just have side-effectless code in our modern day Lisps, so the fact that all arguments passed to unless are evaluated, whether or not we want them to, is problematic. In fact, even in McCarthy's LISP, this could be a problem if, say, evaluating one of the arguments took ages to do, and we'd only want to do it rarely. But it's especially a problem with side-effects.

So we want our unless to evaluate and return q only if p is false. This we can't do if we pass q and p as arguments to a function.

But we can quote them before we pass them to our function, preventing their evaluation. And we can use the power of eval (also defined, using only the primitives and other functions defined with the primitives later in the referenced paper) to evaluate what we need to, when we need to.

So we have a new unless:

(defn unless [p q] 
    (cond (eval p) nil 
          true (eval q)))

And we use it a little differently:

(unless (quote false) (quote (println "squid!")))
=> "squid" nil
(unless (quote true) (quote (println "squid!")))
=> nil

And there you have what could generously be called a macro.


But this isn't defmacro or the equivalent in other languages. That's because on McCarthy's machine, there wasn't a way to execute code during compile-time. And if you were evaluating your code with the eval function, it couldn't know not to evaluate arguments to a "macro" function. There wasn't the same differentiation between the reading and the evaluating as there is now, though the idea was there. The ability to "re-write" code was there, in the coolness of quote and the list operations in conjunction with eval, but it wasn't interned in the language as it is now (I'd call it syntactic sugar, almost: just quote your arguments, and you've got the power of a macro-system right there.)

I hope I've answered your question without trying to define a decent defmacro with those primitives myself. If you really want to see that, I'd point you to the hard-to-grok source for defmacro in the Clojure source, or Google around some more.

Isaac
  • 15,783
  • 9
  • 53
  • 76
2

Explaining it fully in all its details would require an awful lot of space and time for an answer here, but the outline is really pretty simple. Every LISP eventually has in its core something like the READ-EVAL-PRINT loop, which is to say something that takes a list, element by element, interprets it, and changes state -- either in memory or by printing a result.

The read part looks at each element read and does something with it:

(cond ((atom elem)(lambda ...))
      ((function-p elem) (lambda ...)))

To interpret macros, you simply (?) need to implement a function that puts the template text of the macro somewhere in storage, a predicate for that repl loop -- which means simply defining a function -- that says "Oh, this is a macro!", and then copy that template text back into the reader so it's interpreted.

If you really want to see the hairy details, read Structure and Interpretation of Computer Programs or read Queinnec's Lisp in Small PIeces.

Charlie Martin
  • 110,348
  • 25
  • 193
  • 263
  • Does SCICP go in depth about macros? I can't find much mention of them in the PDF with a quick search. – Isaac Aug 21 '10 at 04:40
  • This may be an oversimplification, but are you saying that if I wrote my own eval function I'd be 80% of the way there, provided my eval function knew what a macro declaration was, and what a subsequent call to that macro was. – hawkeye Aug 21 '10 at 13:32
  • If you want the semantics of macros to match CL/Clojure, `eval` would first call `macroexpand-all` on the form and then pass the result to a primitive eval that doesn't need to know about macros. I have implemented a small lisp this way and it works quite well. – Brian Aug 21 '10 at 14:48
  • @Brian oh nice, that's pretty interesting. I'm considering doing the same myself: what language did you write it in? And is the source posted somewhere? – Isaac Aug 21 '10 at 20:15
  • @Issac Hodes: The repl and primitive eval are written in Clojure, the fancy eval and macroexpansion code are written in itself. I just made the project available at http://github.com/qbg/strange – Brian Aug 22 '10 at 04:08