|
home—info—lectures—exams—hws—archive
Macros are code which generates other code (in the same source-language).
Possible reasons to want to write a macro:
You want to write a function, but one which short-circuits:
(define (implies a b) (or (not a) b)) |
We want:
(define-macro (implies a b) (or (not a) b)) |
; The function 'add3': (λ1 + 3 ____) ; Extract all the positive numbers from 'nums': (filter (λ1 > ____ 0) nums) |
C has a primitive system, based purely on modifying strings. (It doesn't let you work on the level of syntax trees at all.) Before compiling a C program, the "c pre-processor" (cpp) makes a first pass where it does some string-substitution. (Use 'gcc -E' to show the results of pre-processing only.)
1 #include <stdio.h> 2 3 #define PI 2.14159+1 4 #define RADIANS_PER_DEGREE 2*PI/360 5 #define MAX(a,b) a >= b ? a : b 6 #define swap(x,y) {int tmp; tmp = x; x = y; y = tmp;} 7 8 int main() { 9 double p = 360 * RADIANS_PER_DEGREE; 10 printf( "The max is: %g.\n", 1/MAX(p++,PI) ); 11 12 13 const int ETM = 15; // #business days between "every third Monday" 14 // This next line doesn't compile, complaining about a hyphen (?!): 15 // const int EOF = 10; // days in the pay period: "Every Other Friday". 16 // Because the pre-processor is oblivious to C, we get weird error messages. 17 18 19 20 int a = 5, b = 99; 21 printf( "Before swap: a=%d, b=%d.\n", a, b ); 22 swap(a,b); 23 printf( "After swap: a=%d, b=%d.\n", a, b ); 24 25 // Happily: swap(++a,b) gives a syntax-error. 26 27 // Uh-oh! 28 int tmp = 5; 29 b = 99; 30 printf( "Before swap: tmp=%d, b=%d.\n", a, b ); 31 swap(tmp,b); 32 printf( "After swap: tmp=%d, b=%d.\n", a, b ); 33 } |
Note that one relatively-more-robust use of the preprocessor is conditional compilation:
#define USING_WINDOWS_VISTA #ifdef (USING_WINDOWS_XP) // code which *only* gets compiled on some platforms #define MAX_DISK_SIZE 300 void writeErrorLog( string msg ) { /* XP-specific code */ } #else void writeErrorLog( string msg ) { /* (non-XP)-specific code */ } #endif |
#if VERBOSE >=2 print("trace message"); #endif |
Overall, C's preprocessor is a failed experiment: rather than a tool which does text-substitution (and knows nothing about the language -- scoping, variables, etc.), it's better to have features like named constants, module systems which search a library for declarations but don't actually include the code, and real functions (which you can suggest that the compiler inline). Conditional compilation can be done by having built-in functions like "getCurrentOS" plus an aggressive compiler.
Further abilities (and other sorts of pitfalls) are mentioned on wikipedia.
Now that we have the need for 'smarter' macros, here are some applications we might use for them: - I wish 'let*' allowed me define functions w/o explicilty typing 'lambda'. - l1 - Little Languages: a macro language for FSMs - auto-generate getters, setters, public/private fields, constructor... - auto-generate the boring code in 'eval', 'equals', ... (Though, see scheme's partial solution in L3-soln.ss) - when using the delegate pattern Reflection vs Macros: - Reflection is a way of 'writing code expands to other code' - less powerful in some ways: you can't make arb. macros, only produce method-calls to existing methods. - However, it's more powerful in some ways: you don't need to know field-names etc in advance.
How well does this C pre-processor macro work?
#define swap(a,b) int tmp = a; a = b; b = tmp; |
The real nail in the coffin: What if somebody else already was using a variable named 'tmp'? And moreover, what would swap(tmp,b) expand to?
We say this macro is non-hygienic, because the introduced-variable 'tmp' can conflict with existing variables.
(One of the most powerful things about regular functions is that they have their own scope; programming where all variables are global (e.g. assembly, early BASIC, early FORTRAN, …) means it's hard to write large programs which don't use conflicting variable names. It's a giant step backwards to add global namespaces.)
void swap( int& a, int& b ) { // a,b are actually references to the real variables. int tmp = a; a = b; b = tmp; } |
void betterSwap( int* a, int* b ) { int tmp; tmp = *a; *a = *b; *b = tmp; } // call this by: int a = 5, b = 99; betterSwap(&a,&b); |
; Think: ; (define (swap a b) ; (let* {[tmp a]} ; (begin ; (set! a b) ; (set! b tmp)))) ; (define-syntax swap (syntax-rules () ((swap a b) (let ((tmp a)) ; a hygienic 'tmp' (set! a b) (set! b tmp))))) (define tmp 5) (define b 99) (swap tmp b) b tmp |
It turns out this mysterious syntax-rules allows pattern-matching (just like prolog!). Here's an example, based on this page; it re-writes let as lambda, as we discussed earlier:
(define-syntax my-let (syntax-rules () ( (my-let ((id expr) ...) body) ; before ((lambda (id ...) body) expr ...) ; after ))) |
(my-let {[a 3] [id1 5] [c 7] } (+ a id1 c)) ; => ((lambda (a id1 c) (+ a id1 c)) 3 5 7) ; => 15 |
Challenge: write define-and-pass-by-reference, which is just like define except that all arguments are actually passed by references. That is,
(define-and-pass-by-reference (swapper a b) …) |
Some personal favorite macros:
A personal favorite macro: I often find myself writing functions of one argument, like
; Suppose I have a list 'data' and a threshold 'n'; ; grab all the elements of 'data' bigger than 'n': (filter (lambda (x) (> x n)) data) |
(filter (l1 > x n) data) |
But macros can be used for more than programmers: it can be used to introduce entire scripting languages, inside (say) Scheme! Here is an example -- taken from Krishnamurthi's manifesto Swine Before Perl: Suppose we want to implement finite state machines. While we could have our non-programming expert friends learn our own programming language plus our own libraries, or we could write a program which takes in strings and interprets them, there is a third approach: let them write something that looks like a FSM spec, but is translated directly into Scheme. For example, here's a boring traffic-light automoton
(define parity (automaton s00 (s00 T : (a -> s10) (b -> s01)) (s01 F : (a -> s11) (b -> s00)) (s10 F : (a -> s00) (b -> s11)) (s11 F : (a -> s01) (b -> s10)))) (parity '(a a b a b a)) ; yields true (parity '(a a b a b a b)) ; yields false |
(define-syntax automaton (syntax-rules (-> :) [(automaton init-state (state accepting? : (cndn -> new-state) ...) ...) (letrec ([state (lambda (input) (if (empty? input) (symbol=? accepting? 'T) (case (first input) [(cndn) (new-state (rest input))] ... [else false])))] ; No listed transition: fail. ...) init-state)])) |
We will refer to Notes from Dr.Okie on:
Also, a particularly nice Prolog tutorial is at learnPrologNow.org.
1 Actually, I use the variable name __ instead of x, reminiscent of "fill in the blank (with the argument)": (filter (l1 > __ n) data) ↩
(define-syntax (l1 stx) (syntax-case stx () [(src-l1 proc args ...) ; This nested syntax-case is so that __ has the same ; context/scope as proc,args. (That is, we ; don't want to introduce a hygienic __ that is really ; different from any __ occuring in proc,args.) ; So create a syntax-object __ that takes context from src-l1, ; and then return a lambda which binds that __. ; (syntax-case (datum->syntax #'src-l1 '__) () [__ (syntax/loc #'src-l1 (lambda (__) (proc args ...)))])])) ; There might be a cleaner way of writing the above |
home—info—lectures—exams—hws—archive
©2009, Ian Barland, Radford University Last modified 2009.Apr.24 (Fri) |
Please mail any suggestions (incl. typos, broken links) to ibarlandradford.edu |