7 Robust macros: syntax-parse
Functions can be used in error. So can macros.
7.1 Error-handling strategies for functions
With plain old functions, we have several choices how to handle misuse.
1. Don’t check at all.
> (define (misuse s) (string-append s " snazzy suffix")) ; User of the function: > (misuse 0) string-append: contract violation
expected: string?
given: 0
argument position: 1st
other arguments...:
" snazzy suffix"
; I guess I goofed, but – what is this "string-append" of which you ; speak??
The problem is that the resulting error message will be confusing. Our user thinks they’re calling misuse, but they’re getting an error message from string-append. In this simple example they could probably guess what’s happening, but in most cases they won’t.
2. Write some error handling code.
> (define (misuse s) (unless (string? s) (error 'misuse "expected a string, but got ~a" s)) (string-append s " snazzy suffix")) ; User of the function: > (misuse 0) misuse: expected a string, but got 0
; I goofed, and understand why! It's a shame the writer of the ; function had to work so hard to tell me.
Unfortunately the error code tends to overwhelm and/or obscure our function definition. Also, the error message is good but not great. Improving it would require even more error code.
3. Use a contract.
> (define/contract (misuse s) (string? . -> . string?) (string-append s " snazzy suffix")) ; User of the function: > (misuse 0) misuse: contract violation
expected: string?
given: 0
in: the 1st argument of
(-> string? string?)
contract from: (function misuse)
blaming: program
(assuming the contract is correct)
at: eval:131.0
; I goofed, and understand why! I'm happier, and I hear the writer of ; the function is happier, too.
This is the best of both worlds.
The contract is simple and concise. Even better, it’s declarative. We say what we want to happen, not how.
On the other hand the user of our function gets a very detailed error message. Plus, the message is in a standard, familiar format.
4. Use Typed Racket.
> (: misuse (String -> String))
> (define (misuse s) (string-append s " snazzy suffix")) > (misuse 0) eval:3:0: Type Checker: type mismatch
expected: String
given: Zero
in: 0
Even better, Typed Racket can catch usage mistakes up-front at compile time.
7.2 Error-handling strategies for macros
For macros, we have similar choices.
1. Ignore the possibility of misuse. This choice is even worse for macros. The default error messages are even less likely to make sense, much less help our user know what to do.
2. Write error-handling code. We saw how much this complicated our macros in our example of Using dot notation for nested hash lookups. And while we’re still learning how to write macros, we especially don’t want more cognitive load and obfuscation.
3. Use syntax-parse. For macros, this is the equivalent of using contracts or types for functions. We can declare that input pattern elements must be certain kinds of things, such as an identifier. Instead of "types", the kinds are referred to as "syntax classes". There are predefined syntax classes, plus we can define our own.
7.3 Using syntax-parse
November 1, 2012: So here’s the deal. After writing everything up to this point, I sat down to re-read the documentation for syntax-parse. It was...very understandable. I didn’t feel confused.
Why? The documentation has a nice Introduction with many simple examples, followed by an Examples section illustrating many real-world scenarios.
Update: Furthermore, Ben Greenman has created a package whose docs provide an excellent set of even more Syntax Parse Examples.
Furthermore, everything I’d learned up to this point prepared me to appreciate what syntax-parse does, and why. The details of how to use it seem pretty straightforward, so far.
This might well be a temporary state of me "not knowing what I don’t know". As I dig in and use it more, maybe I’ll discover something confusing or tricky. If/when I do, I’ll come back here and update this.
But for now I’ll focus on improving the previous parts.