Fear of Macros

Fear of Macros

1 Introduction
I learned Racket after 25 years of doing C/C++ imperative programming.
Some psychic whiplash resulted.
"All the parentheses" was actually not a big deal. Instead, the first +
1 Introduction
I learned Racket after 25 years of doing C/C++ imperative programming.
Some psychic whiplash resulted.
"All the parentheses" was actually not a big deal. Instead, the first mind warp was functional programming. Before long I wrapped my brain around it, and went on to become comfortable and effective with many other aspects and features of Racket.
But two final frontiers remained: Macros and continuations.
I found that simple macros were easy and understandable, plus there @@ -60,7 +60,7 @@ which is #’(displayln "hi")?
> (define-syntax (say-hi stx) #'(displayln "hi")) > (say-hi) hi
When Racket expands our program, it sees the occurrence of (say-hi), and sees it has a transformer function for that. It calls our function with the old syntax, and we return the new syntax, -which is used to evaluate and run our program.
3.2 What’s the input, Kenneth?
Our examples so far ignored the input syntax, and output a fixed +which is used to evaluate and run our program.
3.2 What’s the input?
Our examples so far ignored the input syntax, and output a fixed syntax. But usually we want to transform the input to something else.
Let’s start by looking closely at what the input actually is:
> (define-syntax (show-me stx) (print stx) #'(void)) > (show-me '(i am a list)) #<syntax:10:0 (show-me (quote (i am a list)))>
The (print stx) shows what our transformer is given: a syntax object.
A syntax object consists of several things. The first part is the
s-expression representing the code, such as '(i am a list). Racket (and Scheme and Lisp) expressions are s-expressions—
There are a variety of functions available to access a syntax object:
> (define stx #'(if x (list "true") #f)) > (syntax->datum stx) '(if x (list "true") #f)
> (syntax-e stx) '(#<syntax:11:0 if> #<syntax:11:0 x> #<syntax:11:0 (list "true")> #<syntax:11:0 #f>)
> (syntax->list stx) '(#<syntax:11:0 if> #<syntax:11:0 x> #<syntax:11:0 (list "true")> #<syntax:11:0 #f>)
> (syntax-source stx) 'eval
> (syntax-line stx) 11
> (syntax-column stx) 0
When we want to transform syntax, we’ll generally take the pieces we were given, maybe rearrange their order, perhaps change some of the pieces, and often introduce brand-new pieces.
3.3 Actually transforming the input
Let’s write a transformer function that reverses the syntax it was -given:
> (define-syntax (reverse-me stx) (datum->syntax stx (reverse (cdr (syntax->datum stx))))) > (reverse-me "backwards" "am" "i" values) "i"
"am"
"backwards"
Understand Yoda, can we. Great, but how does this work?
First we take the input syntax, and give it to +given:
> (define-syntax (reverse-me stx) (datum->syntax stx (reverse (cdr (syntax->datum stx))))) > (reverse-me "backwards" "am" "i" values) "i"
"am"
"backwards"
Understand Yoda, can we. Great, but how does this work?
First we take the input syntax, and give it to syntax->datum. This converts the syntax into a plain old list:
> (syntax->datum #'(reverse-me "backwards" "am" "i" values)) '(reverse-me "backwards" "am" "i" values)
Using cdr slices off the first item of the list, reverse-me, leaving the remainder: ("backwards" "am" "i" values). Passing that to reverse changes it to (values "i" "am" "backwards"):
> (reverse (cdr '("backwards" "am" "i" values))) '(values "i" "am")
Finally we use syntax->datum to convert this back to syntax:
> (datum->syntax #f '(values "i" "am" "backwards")) #<syntax (values "i" "am" "backwards")>
That’s what our transformer function gives back to the Racket -compiler, and that syntax is evaluated:
> (values "i" "am" "backwards") "i"
"am"
"backwards"
3.4 Compile time vs. run time
(define-syntax (foo stx) (make-pipe)) ;This is not run time.
Normal Racket code runs at ... run time. Duh.
Instead of "compile time vs. run time", you may hear it +compiler, and that syntax is evaluated:
> (values "i" "am" "backwards") "i"
"am"
"backwards"
3.4 Compile time vs. run time
(define-syntax (foo stx) (make-pipe) ;This is not run time. #'(void)) Normal Racket code runs at ... run time. Duh.
Instead of "compile time vs. run time", you may hear it described as "syntax phase vs. runtime phase". Same difference.
But a syntax transformer is run by the Racket compiler, as part of the process of parsing, expanding and understanding your code. In other words, your syntax transformer function is evaluated at compile time.
This aspect of macros lets you do things that simply aren’t possible @@ -115,20 +115,32 @@ something like racket/match, we have to require it ourselves—
and require it for compile time. Instead of using plain (require racket/match), the way to say this is to use (require (for-syntax racket/match))— the for-syntax -part meaning, "for compile time". So let’s try that:
> (require (for-syntax racket/match))
> (define-syntax (our-if-using-match-v2 stx) (match (syntax->list stx) [(list _ condition true-expr false-expr) (datum->syntax stx `(cond [,condition ,true-expr] [else ,false-expr]))])) > (our-if-using-match-v2 #t "true" "false") "true"
To review:
Syntax transformers work at compile time, not run time. The good +part meaning, "for compile time".
So let’s try that:
> (require (for-syntax racket/match))
> (define-syntax (our-if-using-match-v2 stx) (match (syntax->list stx) [(list _ condition true-expr false-expr) (datum->syntax stx `(cond [,condition ,true-expr] [else ,false-expr]))])) > (our-if-using-match-v2 #t "true" "false") "true"
3.5 begin-for-syntax
We used (require (for-syntax racket/match)) to +require match because we needed to use +match at compiler time.
What if we wanted to make some helper function that could be used by a +macro? One way to do that is put it in another module, and +require it using for/syntax, just like we did for +match.
If we want to put the helper in the same module, we can’t simply +define it and use it – the definition will exist at run +time, but we need it at compile time. The answer is to put the +definition of the helper function(s) inside begin-for-syntax:
(begin-for-syntax (define (my-helper-function x) (* 2 x)) (define-syntax (my-macro stx) (my-helper-function ....) ....)) To review:
Syntax transformers work at compile time, not run time. The good news is this means we can do things like delay evaluation, and implement forms like if which simply couldn’t work properly -as run time functions.
Some other good news is that there isn’t some special, weird language +as run time functions.
Some other good news is that there isn’t some special, weird language for writing syntax transformers. We can write these transformer -functions using familiar Racket code. The semi-bad news is that the -familiarity can make it easy to forget that we’re not working at run -time. Sometimes that’s important to remember. For example only -racket/base is required for us automatically. If we need other -modules, we have to require them, and we have to require them -for compile time using (require (for-syntax)).
4 Pattern matching: syntax-case and syntax-rules
Most useful syntax transformers work by taking some input syntax, and +functions using familiar Racket code.
The semi-bad news is that the familiarity can make it easy to forget +that we’re not working at run time. Sometimes that’s important to +remember.
For example only racket/base is required for us +automatically. If we need other modules, we have to require them, and +do so for compile time using +(require (for-syntax)).
Similarly, if we define helper functions in the same file/module that +uses them, we need to nestle their definitions inside a +begin-for-syntax form. Doing so makes their definition +available at compile time.
4 Pattern matching: syntax-case and syntax-rules
Most useful syntax transformers work by taking some input syntax, and rearranging the pieces into something else. As we saw, this is -possible but tedious using list accessors such as cadddr. It’s -more convenient and less error-prone to use pattern-matching.
Historically, syntax-case and +possible but tedious using list accessors such as +cadddr. It’s more convenient and less error-prone to use +match to do pattern-matching.
Historically, syntax-case and syntax-parse pattern matching came first. match was added to Racket later.
It turns out that pattern-matching was one of the first improvements to be added to the Racket macro system. It’s called diff --git a/main.rkt b/main.rkt index 9848f8a..3a9db4f 100644 --- a/main.rkt +++ b/main.rkt @@ -453,21 +453,63 @@ So let's try that: (our-if-using-match-v2 #t "true" "false") ] +@subsection{@racket[begin-for-syntax]} + +We used @racket[(require (for-syntax racket/match))] to +@racket[require] @racket[match] because we needed to use +@racket[match] at compile time. + +What if we wanted to define our own helper function to be used by a +macro? One way to do that is put it in another module, and +@racket[require] it using @racket[for/syntax], just like we did with +the @racket[racket/match] module. + +If instead we want to put the helper in the same module, we can't +simply @racket[define] it and use it---the definition would exist at +run time, but we need it at compile time. The answer is to put the +definition of the helper function(s) inside @racket[begin-for-syntax]: + +@codeblock{ +(begin-for-syntax + (define (my-helper-function ....) + ....) + (define-syntax (macro-using-my-helper-function stx) + (my-helper-function ....) + ....)) +} + To review: -Syntax transformers work at compile time, not run time. The good -news is this means we can do things like delay evaluation, and -implement forms like @racket[if] which simply couldn't work properly -as run time functions. +@itemize[ -Some other good news is that there isn't some special, weird language +@item{Syntax transformers work at compile time, not run time. The good +news is this means we can do things rearrange the pieces of syntax +without evaluating them. We can implement forms like @racket[if] that +simply couldn't work properly as run time functions.} + +@item{More good news is that there isn't some special, weird language for writing syntax transformers. We can write these transformer -functions using familiar Racket code. The semi-bad news is that the -familiarity can make it easy to forget that we're not working at run -time. Sometimes that's important to remember. For example only -@racket[racket/base] is required for us automatically. If we need other -modules, we have to require them, and we have to require them -@italic{for compile time} using @racket[(require (for-syntax))]. +functions using the Racket language we already know.} + +@item{The semi-bad news is that the familiarity can make it easy to forget +that we're not working at run time. Sometimes that's important to +remember. + + @itemize[ + + @item{For example only @racket[racket/base] is required for us +automatically. If we need other modules, we have to require them, and +do so @italic{for compile time} using +@racket[(require (for-syntax))].} + + @item{Similarly, if we want to define helper functions in the same +file/module as the macros that use them, we need to wrap the +definitions inside a @racket[begin-for-syntax] form. Doing so makes +them available at compile time.} + + ] +} +] @; ---------------------------------------------------------------------------- @@ -475,8 +517,9 @@ modules, we have to require them, and we have to require them Most useful syntax transformers work by taking some input syntax, and rearranging the pieces into something else. As we saw, this is -possible but tedious using list accessors such as @racket[cadddr]. It's -more convenient and less error-prone to use pattern-matching. +possible but tedious using list accessors such as +@racket[cadddr]. It's more convenient and less error-prone to use +@racket[match] to do pattern-matching. @margin-note{Historically, @racket[syntax-case] and @racket[syntax-parse] pattern matching came first. @racket[match] was @@ -828,8 +871,8 @@ TO-DO. @section{Other questions} -Hopefully I will answer these in the course of the other sections. But -just in case: +Hopefully I will answer these in the course of writing the other +sections. But just in case, here's a running list: @subsection{What's the point of @racket[with-syntax]?} @@ -837,7 +880,7 @@ Done. @subsection{What's the point of @racket[begin-for-syntax]?} -TO-DO. +Done. @subsection{What's the point of @racket[racket/splicing]?}