HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Simplifying Macro-Generating Racket Macro

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
generatingracketmacrosimplifying

Problem

I'm trying to write a small macro library in Racket that extends a few of the 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 in racket/match functions



  • The main macro, generate-debug-version, is what I'm trying to simplify.



  • This macro currently takes one function name (for example, match or match*), and generates a macro that defines a new function with a /debug suffix 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:

(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.