patternMinor
Simplifying Macro-Generating Racket Macro
Viewed 0 times
generatingracketmacrosimplifying
Problem
I'm trying to write a small macro library in Racket that extends a few of the
While this seems to be decent practice for learning macros in Racket, I still find them incredibly daunting. At some point, I wind up just trying everything with no real sense of direction until it works how I want, and that's partially how I built my macros up to their current state, as seen here:
Before asking this question, I was trying to a
racket/match functions I use, by printing which clause was expanded.While this seems to be decent practice for learning macros in Racket, I still find them incredibly daunting. At some point, I wind up just trying everything with no real sense of direction until it works how I want, and that's partially how I built my macros up to their current state, as seen here:
#lang racket
(require
(for-syntax
racket/string
racket/list
racket/syntax
syntax/parse
syntax/parse/experimental/template
syntax/parse/lib/function-header))
;; From answer to my question here:
;; http://stackoverflow.com/a/38577121/1017523
(begin-for-syntax
(define (clauses->numbers stx)
(range (length (syntax->list stx)))))
(define-syntax (generate-debug-version stx)
(syntax-case stx ()
[(_ function-name)
(with-syntax ([new-name (format-id stx "~a/debug" #'function-name)])
#'(define-syntax (new-name stx)
(...
(syntax-parse stx
[(_ id [pattern value] ...)
(with-syntax ([(n ... )
(clauses->numbers #'([pattern value] ... ))])
(syntax/loc stx
(function-name
id
[pattern
(begin
(displayln (format "Matched case ~a" (add1 n)))
value)]
...)))]))))]))
(generate-debug-version match)
(generate-debug-version match*)- The small first macro,
clauses->numbers, determines which clause is expanded inracket/matchfunctions
- The main macro,
generate-debug-version, is what I'm trying to simplify.
- This macro currently takes one function name (for example,
matchormatch*), and generates a macro that defines a new function with a/debugsuffix that prints the expanded clause number.
Before asking this question, I was trying to a
Solution
Starting at the top, you have a lot of unused imports. Fortunately, DrRacket can tell you which imports are unused, since it will highlight them in red when you hover your mouse over them. Additionally, if you click the Check Syntax button, it will color all the unused imports red. Using that, we can trim the import list down to the following set:
Next, let’s take a look at the bulk of your code,
Furthermore, the
Note the replacement of explicit uses of
Now with this in mind, we can start to refactor the bulk of the macro itself. There are a few things that could be improved, staring with the uses of
However, there’s actually an issue here, which is that the use of
Another thing we can improve is that we can use the
While we’re discussing names,
Okay, what now? Well, while
```
(begin-for-syntax
(define-splicing-syntax-class numbered-clauses
#:attributes [[pattern 1] [value 1] [n 1]]
#:description #f
[pattern {~seq [pattern value] ...}
#:with [n ..
(require (for-syntax racket/list
racket/syntax
syntax/parse))Next, let’s take a look at the bulk of your code,
generate-debug-version. First of all, it’s a bit odd that you’re using syntax-case for the outer macro, but syntax-parse for the inner one. Just use syntax-parse everywhere; there’s really no reason to ever use syntax-case in Racket.Furthermore, the
syntax/parse/define module provides a nice define-syntax-parser abbreviated form, which helps to simplify things slightly and remove some redundancy:(require (for-syntax racket/list
racket/syntax)
syntax/parse/define)
(define-syntax-parser generate-debug-version
[(_ function-name)
(with-syntax ([new-name (format-id this-syntax "~a/debug" #'function-name)])
#'(define-syntax-parser new-name
(...
[(_ id [pattern value] ...)
(with-syntax ([(n ... )
(clauses->numbers #'([pattern value] ... ))])
(syntax/loc this-syntax
(function-name
id
[pattern
(begin
(displayln (format "Matched case ~a" (add1 n)))
value)]
...)))])))])Note the replacement of explicit uses of
stx with uses of this-syntax, which always refers to the current piece of syntax being matched within a syntax-parse form.Now with this in mind, we can start to refactor the bulk of the macro itself. There are a few things that could be improved, staring with the uses of
with-syntax. Conveniently, syntax/parse permits #:with clauses after patterns themselves, which act sort of like wrapping the body with with-syntax, but they can use syntax-parse patterns, and the parser will backtrack if they fail. This lets us simplify the code even further:(define-syntax-parser generate-debug-version
[(_ function-name)
#:with new-name (format-id this-syntax "~a/debug" #'function-name)
#'(define-syntax-parser new-name
(...
[(_ id [pattern value] ...)
#:with (n ...) (clauses->numbers #'([pattern value] ...))
(syntax/loc this-syntax
(function-name
id
[pattern
(begin
(displayln (format "Matched case ~a" (add1 'n)))
value)]
...))]))])However, there’s actually an issue here, which is that the use of
format-id really shouldn’t use this-syntax at all. Instead, it should pull lexical context from #'function-name itself, since the provided identifier should control where the generated identifier’s lexical context comes from. (For an example of where this can be important, try using the define-tracing-match* form from the end of this answer without this change, and you’ll see what the issue is.)(define-syntax-parser generate-debug-version
[(_ function-name)
#:with new-name (format-id #'function-name "~a/debug" #'function-name)
; ...
])Another thing we can improve is that we can use the
id syntax class for the function-name pattern, which will ensure that the generate-debug-version macro is actually provided an identifier, and will raise a syntax error if it isn’t. Additionally, function-name really isn’t a great name for that pattern, since match is not a function, it is a form. Let’s fix that:(define-syntax-parser generate-debug-version
[(_ form-id:id)
#:with debug-id (format-id #'form-id "~a/debug" #'form-id)
#'(define-syntax-parser debug-id
; ...
)])While we’re discussing names,
generate-debug-version isn’t a very good one. For one thing, it doesn’t just generate a debug form, it defines one. For another, it specifically generates debug versions of match-like forms, nothing else, so that should probably be included in the name, too. I picked the name define-tracing-match, but you could pick a similar name if you wished.Okay, what now? Well, while
clauses->numbers works, it could honestly be better. It’s really nice that we can use syntax patterns to write such a declarative style of macro, but clauses->numbers isn’t declarative at all, it’s completely procedural. To help fix that, we can write a splicing syntax class which will number the clauses for us, lifting out the procedural component into a separate piece. That looks like this:```
(begin-for-syntax
(define-splicing-syntax-class numbered-clauses
#:attributes [[pattern 1] [value 1] [n 1]]
#:description #f
[pattern {~seq [pattern value] ...}
#:with [n ..
Code Snippets
(require (for-syntax racket/list
racket/syntax
syntax/parse))(require (for-syntax racket/list
racket/syntax)
syntax/parse/define)
(define-syntax-parser generate-debug-version
[(_ function-name)
(with-syntax ([new-name (format-id this-syntax "~a/debug" #'function-name)])
#'(define-syntax-parser new-name
(...
[(_ id [pattern value] ...)
(with-syntax ([(n ... )
(clauses->numbers #'([pattern value] ... ))])
(syntax/loc this-syntax
(function-name
id
[pattern
(begin
(displayln (format "Matched case ~a" (add1 n)))
value)]
...)))])))])(define-syntax-parser generate-debug-version
[(_ function-name)
#:with new-name (format-id this-syntax "~a/debug" #'function-name)
#'(define-syntax-parser new-name
(...
[(_ id [pattern value] ...)
#:with (n ...) (clauses->numbers #'([pattern value] ...))
(syntax/loc this-syntax
(function-name
id
[pattern
(begin
(displayln (format "Matched case ~a" (add1 'n)))
value)]
...))]))])(define-syntax-parser generate-debug-version
[(_ function-name)
#:with new-name (format-id #'function-name "~a/debug" #'function-name)
; ...
])(define-syntax-parser generate-debug-version
[(_ form-id:id)
#:with debug-id (format-id #'form-id "~a/debug" #'form-id)
#'(define-syntax-parser debug-id
; ...
)])Context
StackExchange Code Review Q#147067, answer score: 4
Revisions (0)
No revisions yet.