Macros ------ It would be nice to be able to define new special forms in Scheme -- functions that don't evaluate their arguments, or do so in a controlled way, like if and cond. For example, we may wish to have a special form unless such that (unless test body) evaluates test, and then only if the test is #f, evaluates and returns the value of body. We could use if or cond for this: we could write (if (not test) body #f) which does what we want. But (unless test body) looks better. It would be nice if we could define it. Note that we cannot define unless as a function. In particular, if we defined: (define (unless test body) (if (not test) body)) then when whenever we call unless, the body will be evaluated regardless of the outcome of the test. It would also be convenient to be able to change the values of variables in the current environment in a function call (call-by-reference or call-by-name). For example, we might want to have a function inc! that when applied to a variable x increments it; i.e., (inc! x) would have the same effect as (set! x (inc x)). Right now there is no way to do these things using the Scheme constructs we have seen. You might think the following definition will work: (define (inc! x) (set! x (+ 1 x))) but it doesn't. The definition of inc! above set!s the value of the local x (the parameter), not the variable whose value is passed to it. inc! cannot be written as a function since it needs access to the variable in the argument, not its value. (You could do this in C with pointers.) In problem set 6, you'll define a bunch of special forms by modifying the interpreter so that we get the right behavior. But we don't always have access to a language's interpreter so this approach is not always viable. Also in problem set 6, you'll define a bunch of special forms by modifying a compiler. The compiler will translate certain special forms into expressions that have the appropriate behavior when run using the interpreter. For instance, the interpreter does not understand "let". Rather, when we read in an expression, we compile out all let-expressions. In particular there is a generic function for compiling expressions that looks like this: ;;; Default behavior (defmethod (ts:compile exp) exp) ;;; Behavior for combinations (defmethod (ts:compile (exp )) (ts:compile-combination (head exp) (tail exp))) ;;; Default behavior for combinations (defmethod (ts:compile-combination operator args) (map ts:compile (cons operator args))) ;;; We compile (let ((x1 e1) (x2 e2) ... (x1 en)) exp1 ... expm) into ;;; ((lambda (x1 x2 ... xn) exp1 ... expm) e1 e2 ... en). (defmethod (ts:compile-combination (operator = 'let) exps) (if (>= (length exps) 2) (let* ((decls (first exps)) (body (tail exps)) (xs-and-es (split-bindings decls)) (xs (car xs-and-es)) (es (cdr xs-and-es))) (ts:compile (cons (append (list 'lambda xs) body) es))) (ts:compiler-error (cons 'let exps) "malformed let-expression"))) ;;; split a list of bindings ((x1 e1) (x2 e2) ... (xn en)) into a pair ;;; of a list of variables and a list of expressions ;;; ((x1 x2 ... xn) (e1 e2 ... en)). Check that the variables are in ;;; fact symbols. (define (split-bindings bindings) (define (split ds xs es) (if (null? ds) (cons (reverse xs) (reverse es)) (let ((d (head ds))) (if (and (= (length d) 2) (symbol? (first d))) (split (tail ds) (cons (first d) xs) (cons (second d) es)) (ts:compiler-error d "malformed binding"))))) (split bindings null null)) But of course, we don't always have access to a compiler either. --------------------------------------------------------------------- Fortunately, Scheme (and DrSwindle) provide a linguistic mechanism for adding new special forms to the language without having to have access to the interpreter or compiler. The mechanism is called a MACRO: Macros are like functions, except they don't evaluate their arguments, but textually substitute them unevaluated into the body, then evaluate the result. These special functions construct code out of other code -- operate on Scheme expressions as list structures. Macros *are* part of Scheme/Swindle. Ordinary functions: 1. Evaluate the arguments 2. Apply function to (values of) the arguments 3. Return the result of step 2 Macros: 1. Substitute the (unevaluated) arguments *textually* in the body 2. Evaluate the result 3. Return the result of step 2 *as an expression* Basic idea (function): Example: (square x) The head is *not* a macro, so we evaluate square and x in current env Then apply [value of square] to [value of x] Basic idea (macro): Example: (inc! z) The head *is* a macro, so we create the body (set! z (inc z)) which is (set! x (inc x)) with the *unevaluated* z substituted for x, which we then evaluate ---------------------------------------------------------------------- 1. The body is evaluated twice, once to do the textual substitution and once to compute the value. 2. The first evaluation is done in the environment of the macro closure, the second is done in the environment of the call. (This is usually what we want.) A macro object is created by evaluating (defmacro (macro-name params) pre-body) where pre-body is something that will evaluate to the desired body when the UNEVALUATED arguments are bound to the params. The resulting body will then be evaluated in the *original* environment (i.e., the environment of the macro call). For example, we want (inc! Z) to expand to (set! Z (inc Z)), and then we want that to be evaluated in the *current* environment. We would define (defmacro (inc! x) (list 'set! x (list '+ 1 x))) Here the pre-body is (list 'set! x (list '+ 1 x)) Now, if you type (inc! z) the unevaluated symbol z will be bound to the parameter x of the macro and the pre-body will be evaluated in that environment, giving (set! z (inc z)) Then that expression will be evaluated in the *current* environment, which has the effect of incrementing z in the current environment. ----------------------------------------------------------------- Similarly, for unless, we would define (defmacro (unless tst bdy) (list 'if (list 'not tst) bdy '#f)) Then when we evaluate (unless (= x 0) (/ 2 x)) the pre-body (list 'if (list 'not tst) bdy '#f) is evaluated in an environment with bindings tst:(= x 0) and bdy:(/ 2 x), the unevaluated lists, to give (if (not (= x 0)) (/ 2 x) #f) ---that's the first evaluation---then that expression is evaluated in the current environment. ----------------------------------------------------------------- A macro gets expanded when its name appears as the first position of a compound expression, as in (inc! x), which looks just like an ordinary function application, except the thing in first position is a symbol naming the macro. Macro application calls a procedure to evaluate the macro body (which is the pre-body discussed above) in the environment of the closure with the macro parameters bound to the unevaluted arguments, then evaluates the result in the *calling* environment. ---------------------------------------------------------------------- So, this lets us define new special forms. Try out inc! (define x 12) Then (inc! x) ;; now x is 13 This is very much like procedure calls, except 1. Macro gets the text of the call, not the value 2. Macro is evaluated *twice*. ---------------------------------------------------------- WARNING: * Macros are incredibly dangerous! * They are almost never the right thing to use! Many languages have them * They can do things function calls can't. Here's C-style: C hackers are required by federal law to be efficiency freaks, * Macros in C save the cost of a function call * Which is pretty high in C. #define square(x) x*x For example, square(4) --expands-to--> 4*4 The parameter x is replaced by the TEXT of its actual argument, 4. But this is DOOMED: square(n+2) --enpands-to--> n+2*n+2 which parses as n+(2*n)+2, which is hardly ever equal to (n+2)*(n+2). In C -- not Scheme, Scheme's fully parenthesized -- you have to write #define square(x) ((x)*(x)) and if you forget you are DOOMED. Even this doesn't help: Suppose Expensive(n) takes a week to compute. square(Expensive(n)) --expands-to--> ((Expensive(n))*(Expensive(n))) which means you call it *twice*, so it takes two weeks to compute. --> But, you did save the function call. Yay! Anyways, macros are too dangerous to use when you can avoid it. Basically should restrict use to defining new special forms. ---------------------------------------------------------------------- It turns out that Scheme has an extremely powerful macro language that lets you define almost all of the special forms, such as let, let*, letrec, etc. using macros. In fact, Eli implemented almost all of the DrSwindle special forms, e.g. defstruct, defclass, etc. as macros! Thus, he was able to extend the functional language to an object-oriented language without having to modify the underlying MzScheme interpreter or compiler. In general, meta-programming facilities, such as macros, provide a great way to customize a language to a particular domain or to add new features to the language. Next time, we will talk about adding new features, promises and streams, to the language and see that doing so provides us with some very useful mechanisms. Again, we're able to do this without having to modify the underlying interpreter or compiler. We will talk about the more general macro language later on. For now we will stick with the simpler defmacro facility. ---------------------------------------------------------------------- SUMMARY: * Think of macros as TEXTUAL SUBSTITUTION done BEFORE evaluator gets an expression to evaluate. * Macros greatly extend the expressive power of languages. - They are very powerful - They are very tricky to think about