When writing a macro that uses syntax/parse
, I have created a splicing syntax class that captures options that may be provided to the macro. These options are all optional, and they may be provided in any order. Using the ~optional
ellipsis head pattern makes this easy enough:
(define-splicing-syntax-class opts
(pattern (~seq (~or (~optional (~seq #:a a))
(~optional (~seq #:b b))
(~optional (~seq #:x x))
(~optional (~seq #:y y)))
...))
However, there is a catch: I want to be able to group these options into two groups: the group containing a
and b
, and the group containing x
and y
. However, the user may still specify the options in any order, so for this example input:
(foobar #:b 3 #:y 7 #:a 2)
I want to be able to produce the following attributes:
first-opts: (#:a 2 #:b 3)
second-opts: (#:y 7)
So far, I’ve managed to do this manually using #:with
, but it isn’t pretty:
(define-splicing-syntax-class opts
#:attributes ([first-opts 1] [second-opts 1])
(pattern (~seq (~or (~optional (~seq #:a a))
(~optional (~seq #:b b))
(~optional (~seq #:x x))
(~optional (~seq #:y y)))
...)
#:with (first-opts ...)
#`(#,@(if (attribute a) #'(#:a a) #'())
#,@(if (attribute b) #'(#:b b) #'()))
#:with (second-opts ...)
#`(#,@(if (attribute x) #'(#:x x) #'())
#,@(if (attribute y) #'(#:y y) #'()))))
This can be simplified a little bit using template
from syntax/parse/experimental/template
:
(define-splicing-syntax-class opts
#:attributes ([first-opts 1] [second-opts 1])
(pattern (~seq (~or (~optional (~seq #:a a))
(~optional (~seq #:b b))
(~optional (~seq #:x x))
(~optional (~seq #:y y)))
...)
#:with (first-opts ...)
(template ((?? (?@ #:a a))
(?? (?@ #:b b))))
#:with (second-opts ...)
(template ((?? (?@ #:a x))
(?? (?@ #:b y))))))
However, this is really just some sugar for the above, and it doesn’t actually address the problem of having to enumerate each option in each clause. If I, for example, added a #:c
option, I would need to remember to add it to the first-opts
group, otherwise it would be completely ignored.
What I really want is some declarative way to group these sets of optional values. For example, I’d like a syntax like this:
(define-splicing-syntax-class opts
#:attributes ([first-opts 1] [second-opts 1])
(pattern (~seq (~or (~group first-opts
(~optional (~seq #:a a))
(~optional (~seq #:b b)))
(~group second-opts
(~optional (~seq #:x x))
(~optional (~seq #:y y))))
...)))
Or, even better, it would be nice if I could use existing primitives, something like this:
(define-splicing-syntax-class opts
#:attributes ([first-opts 1] [second-opts 1])
(pattern (~seq (~or (~and first-opts
(~seq (~optional (~seq #:a a))
(~optional (~seq #:b b))))
(~and second-opts
(~seq (~optional (~seq #:x x))
(~optional (~seq #:y y)))))
...)))
However, neither of those work. Is there any way to do this using the builtins provided by syntax/parse
? If not, is there any simple way to define something like ~group
myself?
There is a way to do that with a ~groups-no-order
pattern expander like this:
(define-splicing-syntax-class opts
#:attributes ([first-opts 1] [second-opts 1])
[pattern (~groups-no-order
[first-opts
(~optional (~seq #:a a))
(~optional (~seq #:b b))]
[second-opts
(~optional (~seq #:x x))
(~optional (~seq #:y y))])])
(syntax-parse #'(foobar #:b 3 #:y 7 #:a 2)
[(foobar opts:opts)
(values #'(opts.first-opts ...)
#'(opts.second-opts ...))])
; #<syntax (#:a 2 #:b 3)>
; #<syntax (#:y 7)>
Where ~groups-no-order
can be defined like this:
#lang racket
(provide ~groups-no-order)
(require syntax/parse
seq-no-order
(for-syntax racket/syntax
syntax/stx))
(define-syntax ~groups-no-order
(pattern-expander
(lambda (stx)
(syntax-case stx ()
[(groups [group-name member-pat ...] ...)
(with-syntax ([ooo (quote-syntax ...)])
(define/with-syntax [[member-tmp ...] ...]
(stx-map generate-temporaries #'[[member-pat ...] ...]))
(define/with-syntax [group-tmp ...]
(generate-temporaries #'[group-name ...]))
#'(~and (~seq-no-order (~and (~seq (~var member-tmp) ooo)
member-pat)
... ...)
(~parse [[(~var group-tmp) ooo] ooo] #'[[member-tmp ooo] ...])
...
(~parse [group-name ooo] #'[group-tmp ooo ooo])
...))]))))
This does the same thing as your first solution using #:with
, but it abstracts that stuff out into a reusable pattern expander.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With