I've implemented my own Lisp on top of node.js, I can run s-expressions like this:
(assert (= 3 (+ 1 2))) (def even? (fn [n] (= 0 (bit-and n 1)))) (assert (even? 4)) (assert (= false (even? 5)))
Now I would like to add macros - the defmacro
function - but this is where I'm stuck. I'm wondering how macro systems are implemented in other Lisps but I couldn't find many pointers (apart from this and this).
I've looked at the Clojure macro system - the Lisp I'm most familiar with - but that seemed too complicated and I couldn't find additional clues that I can readily apply (Clojure macros ultimately compile to byte code which doesn't apply to javascript, also I couldn't make sense of the macroexpand1
function.)
So my question is: given a Lisp implementation without macros but with an AST, how does one add a macro system like Clojure's macro system? Can this macro system be implemented in Lisp, or does it require extra features in the implementation in the host language?
One additional remark: I haven't implemented quote
('
) yet because I couldn't figure out what kind of values should be in the returned list. Should it contain AST elements or objects like Symbol
and Keyword
(the latter being the case for Clojure)?
Macros allow you to extend the syntax of standard LISP. Technically, a macro is a function that takes an s-expression as arguments and returns a LISP form, which is then evaluated.
A macro call involves computation at two times: when the macro is expanded, and when the expansion is evaluated. All the macroexpansion in a Lisp program is done when the program is compiled, and every bit of computation which can be done at compile-time is one bit that won't slow the program down when it's running.
Macros do code transformations at compile time or runtime. That's different from functions. Just look into the Common Lisp standard for many different pre-defined macros and their different syntax. Now think about, why these are macros and not functions.
Rust's macros are very good. They act like Lisp's macros, unlike Haskell's. The fact that Rust has type-classes (“traits”) and sum types (“enums”) and pattern matching is very attractive.
All a macro does is take unevaluated forms as parameters and perform replacement on its body. The trick to implementation a macro system is to tell your compiler to be lazy.
Put in another way, when the compiler encounters a function, it first evaluates its formal parameter list, yields the results and passes them to the function. When the compiler finds a macro, it passes the arguments unevaluated to the body, then performs any computation the body requests, and finally replaces itself with the result of those.
For instance, lets say you have a function:
(defun print-3-f (x) (progn (princ x) (princ x) (princ x)))
and a macro:
(defmacro print-3-m (x) `(progn (princ ,x) (princ ,x) (princ ,x)))
Then you can see the difference straight away:
CL-USER> (print-3-f (rand))
* 234
* 234
* 234
CL-USER> (print-3-m (rand))
* 24
* 642
* 85
To understand why this is, you need to, in a manner of speaking, run the compiler in your head.
When Lisp comes across the function, it builds a tree in which (rand)
is first evaluated and the result passed to the function, which prints said result three times.
On the other hand, when Lisp comes across the macro, it passes the form (rand)
untouched to the body, which returns a quoted list where x
is replaced by (rand)
, yielding:
(progn (princ (rand)) (princ (rand)) (princ (rand)))
and replacing the macro call for this new form.
Here you will find vast amounts of documentation regarding macros in a variety of languages, including Lisp.
This is from Peter Norvig's Paradigms of Artificial Intelligence Programming - an essential tome for any LISP programmers bookshelf.
He assumes you're implementing an Interpreted language, and provides the example of a Scheme Interpreter running in LISP.
The following two examples here show how he adds macros to the primary eval
function (interp
)
Here is the function for interpreting an S-expression prior to dealing with macros:
(defun interp (x &optional env)
"Interpret (evaluate) the expression x in the environment env."
(cond
((symbolp x) (get-var x env))
((atom x) x)
((case (first x)
(QUOTE (second x))
(BEGIN (last1 (mapcar #'(lambda (y) (interp y env))
(rest x))))
(SET! (set-var! (second x) (interp (third x) env) env))
(IF (if (interp (second x) env)
(interp (third x) env)
(interp (fourth x) env)))
(LAMBDA (let ((parms (second x))
(code (maybe-add 'begin (rest2 x))))
#'(lambda (&rest args)
(interp code (extend-env parms args env)))))
(t ;; a procedure application
(apply (interp (first x) env)
(mapcar #'(lambda (v) (interp v env))
(rest x))))))))
And here it is after a macro evaluation has been added (child methods have been in the reference link for clarity
(defun interp (x &optional env)
"Interpret (evaluate) the expression x in the environment env.
This version handles macros."
(cond
((symbolp x) (get-var x env))
((atom x) x)
((scheme-macro (first x))
(interp (scheme-macro-expand x) env))
((case (first x)
(QUOTE (second x))
(BEGIN (last1 (mapcar #'(lambda (y) (interp y env))
(rest x))))
(SET! (set-var! (second x) (interp (third x) env) env))
(IF (if (interp (second x) env)
(interp (third x) env)
(interp (fourth x) env)))
(LAMBDA (let ((parms (second x))
(code (maybe-add 'begin (rest2 x))))
#'(lambda (&rest args)
(interp code (extend-env parms args env)))))
(t ;; a procedure application
(apply (interp (first x) env)
(mapcar #'(lambda (v) (interp v env))
(rest x))))))))
It is interesting to note that the opening Chapter of Christian Queinnec's Lisp In Small Pieces has a very similar function, he calls it eval
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With