150 likes | 380 Views
Macros. “How can you get anything done in [other languages], I think, without macros?” - Paul Graham, 2003. Writing Programs…. … that write programs! Macros work via transformation Macros translate a given LISP expression into another expression Allows for efficient execution.
E N D
Macros “How can you get anything done in [other languages], I think, without macros?” - Paul Graham, 2003
Writing Programs… • … that write programs! • Macros work via transformation • Macros translate a given LISP expression into another expression • Allows for efficient execution
Two Similar Functions (defun print-file (file) (with-open-file (str file) (do ((line (read-line str nil nil) (read-line str nil nil))) ((null line) nil) (format t "~A~%" line) ))) (defun find-first-comment (file) (with-open-file (str file) (do ((line (read-line str nil nil) (read-line str nil nil))) ((null line) nil) (when (eql (char line 0) #\;) (return line)) )))
Desire to Generalize • If this pattern reoccurs often, we would like to do something simpler like (defun print-file (file) (dofile line file (format t "~A~%" line) )) (defun find-first-comment (file) (dofile line file (when (eql (char line 0) #\;) (return line)) )) • But how would you do this with a function? line is being passed as a variable name…
Solution is Macros • What a function does • 1. Evaluate each argument • 2. Evaluate the body of the function • What a macro does • 1. Evaluate body of macro • 2. Evaluate resulting body of expanded macro
Defining Macros • Similar to defun (defmacro macro-name (argument*) S-expr*) (defmacro dofile (var file &rest body) (list 'with-open-file (list 'str file) (list* 'do (list (list var (list 'read-line 'str 'nil 'nil) (list 'read-line 'str 'nil 'nil))) (list (list 'null var) 'nil) body))) • But this is impossibly ugly
Backquote • The backquote (`), like single quote ('), can be used to inhibit evaluation >`(a b c d) (A B C D) >`(setf x (* 3 7)) (SETF X (* 3 7)) • Unlike single quote, can specify partial evaluation by using a comma (,) >`(setf x ,(* 3 7)) (SETF X 21)
Simplified Macro with Backquote • We can make our macro definition more readable with the backquote (defmacro dofile (var file &rest body) `(with-open-file (str ,file) ,(list* 'do `((,var (read-line str nil nil) (read-line str nil nil))) `((null ,var) nil) body))) • Still kind of ugly. Problem is that body can be a list of commands, so we need list*
Splice • The splicing operator (,@) allows the outer parentheses of an expression to be removed >`(+ ,@(cdr '(2 4 6))) (+ 4 6) • Now we can fix our macro (defmacro dofile (var file &rest body) `(with-open-file (str ,file) (do ((,var (read-line str nil nil) (read-line str nil nil))) ((null ,var) nil) ,@body) ))
Macroexpand • A tool for debugging macros, it expands a macro and suppresses second evaluation, returning the expanded expression >(defmacro front (some-list) (list 'car some-list)) FRONT >(macroexpand '(front '(1 2 3))) (CAR '(1 2 3)) T • If your macro definition contains macros in it, then they will also be expanded, which may result in code you don’t recognize
Specializing the Input • Currently we call our macro using (dofile var file body*) • But we can group our arguments like so (dofile (var file) body*) • If we parenthesize them in the macro definition • Also, in macros, &body is preferred in place of &rest, for formatting reasons. So now we have: (defmacro dofile ((var file)&body body) `(with-open-file (str ,file) (do ((,var (read-line str nil nil) (read-line str nil nil))) ((null ,var) nil) ,@body) ))
Problems (1) • This code works: (defun copy (source out) (with-open-file (outstr out :direction :output) (dofile (line source) (format outstr "~A~%" line)))) • But this almost identical code doesn’t (defun copy (source out) (with-open-file (str out :direction :output) (dofile (line source) (format str "~A~%" line))))
Problems (2) • If we expand the macro definition, we see why (defun copy (source out) (with-open-file (str out :direction :output) (with-open-file (str source) (do ((line (read-line str nil nil) (read-line str nil nil))) ((null line) nil) (format str "~A~%" line))) )) • There are name collisions between the variables in the macro and those in the function definition. • The only way to avoid this is by knowing the internals of the macro, which is unacceptable. We need to fix the macro again
Gensym • gensym generates a new symbol name that is guaranteed to be unique • We use it to get unique variable names for our macro (changes in red) (defmacro dofile ((var file) &body body) (let ((str (gensym))) `(with-open-file (,str ,file) (do ((,var (read-line ,str nil nil) (read-line ,str nil nil))) ((null ,var) nil) ,@body) )))
Macroexpand After Gensym >(macroexpand '(dofile (line "file.txt") nil)) (LET ((#:G1478 (OPEN "file.txt"))) (UNWIND-PROTECT (PROGN (BLOCK () (LET ((LINE (READ-LINE #:G1478 NIL NIL))) (TAGBODY #:G1479 (IF (NULL LINE) (RETURN (PROGN NIL))) (TAGBODY NIL) (PSETQ LINE (READ-LINE #:G1478 NIL NIL)) (GO #:G1479))))) (IF #:G1478 (CLOSE #:G1478))))