3
\$\begingroup\$

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 add the ability to supply multiple function names to this macro using ellipses (making the last two lines (generate-debug-versions match match*)), but I can't figure out how in my macro's current state.

Can someone give me advice on how to simplify/clarify my generate-debug-version macro, so I can ultimately rewrite it to accept multiple arguments?

I'm aware of the simpler define-syntax-rule macro tool, but it didn't work in the circumstances I attempted.

\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

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 ...] (range (length (attribute pattern)))]))

You can read more about what syntax classes do and how they work in the extensive documentation, but the basic idea here is to extract out some procedural logic into a reusable pattern than syntax-parse can understand. The #:attributes and #:description options are optional here, but the former helps readability, and the latter helps with error messages.

With that syntax class in place, we can further simplify the main macro by using it:

(define-syntax-parser define-tracing-match
  [(_ form-id:id)
   #:with debug-id (format-id #'form-id "~a/debug" #'form-id)
   #'(define-syntax-parser debug-id
       (...
        [(_ id clause:numbered-clauses)
         (syntax/loc this-syntax
           (form-id id
             [clause.pattern
              (begin
                (displayln (format "Matched case ~a" (add1 'clause.n)))
                clause.value)]
            ...))]))])

This makes our main macro completely declarative (save for the small use of format-id, which is pretty harmless), and it’s pretty much entirely built out of patterns and templates. This is what the Racket macro system is so good at: it lets you pretty much just write what you mean, and you still get great error reporting for when things go wrong.

With all these changes in place, here’s what the final code looks like:

#lang racket

(require (for-syntax racket/list
                     racket/syntax)
         syntax/parse/define)

(begin-for-syntax
  (define-splicing-syntax-class numbered-clauses
    #:attributes [[pattern 1] [value 1] [n 1]]
    #:description #f
    [pattern {~seq [pattern value] ...}
             #:with [n ...] (range (length (attribute pattern)))]))

(define-syntax-parser define-tracing-match
  [(_ form-id:id)
   #:with debug-id (format-id #'form-id "~a/debug" #'form-id)
   #'(define-syntax-parser debug-id
       (...
        [(_ id clause:numbered-clauses)
         (syntax/loc this-syntax
           (form-id id
             [clause.pattern
              (begin
                (displayln (format "Matched case ~a" (add1 'clause.n)))
                clause.value)]
            ...))]))])

A bit more concise, and hopefully a bit clearer, too.


Now for a couple of extras. You mentioned that you wanted to write a generate-debug-versions macro. This wouldn’t be too hard to do with the new version of generate-debug-version by using some well-placed ellipses, actually, but it would probably be even easier to just define a new generate-debug-versions macro (or, since we changed the name, a define-tracing-match* macro) that defers to the existing one.

The easiest way to do this is probably to use the define-simple-macro form, also from syntax/parse/define, which is basically syntax-rules mixed with all the syntax-parse enhancements. The macro itself is trivial:

(define-simple-macro (define-tracing-match* form-id:id ...)
  (begin (define-tracing-match form-id) ...))

Now, it’s extremely easy to define both match/debug and match*/debug at the same time:

(define-tracing-match* match match*)

One final additional thing you can do is adjust the format-id call to copy source location information and syntax properties from the provided form-id, like this:

#:with debug-id (format-id #'form-id "~a/debug" #'form-id
                           #:source #'form-id #:props #'form-id)

By doing this, when you hover your cursor over a use of match/debug in DrRacket, it will draw an arrow to the identifier used with define-tracing-match to generate the debug version in the first place, which is useful.

However, if you wanted to get even fancier, you could skip that step and use the 'sub-range-binders syntax property to convey even more fine-grained information to DrRacket. Specifically, you can adjust the outer macro for define-tracing-match to attach the property as follows:

(define-syntax-parser define-tracing-match
  [(_ form-id:id)
   #:with debug-id (format-id #'form-id "~a/debug" #'form-id)
   (syntax-property
    #'(define-syntax-parser debug-id
        (...
         [(use-id id clause:numbered-clauses)
          (syntax/loc this-syntax
            (form-id id
                     [clause.pattern
                      (begin
                        (displayln (format "Matched case ~a" (add1 'clause.n)))
                        clause.value)]
                     ...))]))
    'sub-range-binders
    (let ([id-len (string-length (symbol->string (syntax-e #'form-id)))])
      (vector (syntax-local-introduce #'debug-id) 0 id-len 0.5 0.5
              (syntax-local-introduce #'form-id) 0 id-len 0.5 0.5)))])

This will make DrRacket draw an arrow for part of the match/debug identifier, the part that shares the same name as the provided one. The best way to illustrate what this does is with a screenshot:

(This is the technique the built-in struct macro uses to get the special binding arrows for its field accessors.)

Admittedly, however, this is pretty advanced macrology, and I probably wouldn’t do something this fancy unless I was distributing it as part of a library, so no worries if you leave that part out.

\$\endgroup\$
1
  • \$\begingroup\$ Wow! Thanks a ton, your answer is unbelievably helpful! I've been trying to read Gred Hendershott's macro guide off and on, and this gives me a ton of practical code transformations to learn more about. I hope I encounter more people on CodeReview as helpful as you :) \$\endgroup\$
    – user36474
    Nov 15, 2016 at 14:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.