8.2.5 More keyword arguments

This section shows how to express the syntax of struct’s optional keyword arguments using syntax-parse patterns.

The part of struct’s syntax that is difficult to specify is the sequence of struct options. Let’s get the easy part out of the way first.

  > (define-splicing-syntax-class maybe-super
      (pattern (~seq super:id))
      (pattern (~seq)))
  > (define-syntax-class field-option
      (pattern #:mutable)
      (pattern #:auto))
  > (define-syntax-class field
      (pattern field:id
               #:with (option ...) '())
      (pattern [field:id option:field-option ...]))

Given those auxiliary syntax classes, here is a first approximation of the main pattern, including the struct options:
  (struct name:id super:maybe-super (field:field ...)
    (~or (~seq #:mutable)
         (~seq #:super super-expr:expr)
         (~seq #:inspector inspector:expr)
         (~seq #:auto-value auto:expr)
         (~seq #:guard guard:expr)
         (~seq #:property prop:expr prop-val:expr)
         (~seq #:transparent)
         (~seq #:prefab)
         (~seq #:constructor-name constructor-name:id)
         (~seq #:extra-constructor-name extra-constructor-name:id)
         (~seq #:omit-define-syntaxes)
         (~seq #:omit-define-values))
    ...)
The fact that expr does not match keywords helps in the case where the programmer omits a keyword’s argument; instead of accepting the next keyword as the argument expression, syntax-parse reports that an expression was expected.

There are two main problems with the pattern above:
  • There’s no way to tell whether a zero-argument keyword like #:mutable was seen.

  • Some options, like #:mutable, should appear at most once.

The first problem can be remedied using ~and patterns to bind a pattern variable to the keyword itself, as in this sub-pattern:
  (~seq (~and #:mutable mutable-kw))
The second problem can be solved using repetition constraints:
  (struct name:id super:maybe-super (field:field ...)
    (~or (~optional (~seq (~and #:mutable) mutable-kw))
         (~optional (~seq #:super super-expr:expr))
         (~optional (~seq #:inspector inspector:expr))
         (~optional (~seq #:auto-value auto:expr))
         (~optional (~seq #:guard guard:expr))
         (~seq #:property prop:expr prop-val:expr)
         (~optional (~seq (~and #:transparent transparent-kw)))
         (~optional (~seq (~and #:prefab prefab-kw)))
         (~optional (~seq #:constructor-name constructor-name:id))
         (~optional
           (~seq #:extra-constructor-name extra-constructor-name:id))
         (~optional
           (~seq (~and #:omit-define-syntaxes omit-def-stxs-kw)))
         (~optional (~seq (~and #:omit-define-values omit-def-vals-kw))))
    ...)
The ~optional repetition constraint indicates that an alternative can appear at most once. (There is a ~once form that means it must appear exactly once.) In struct’s keyword options, only #:property may occur any number of times.

There are still some problems, though. Without additional help, ~optional does not report particularly good errors. We must give it the language to use, just as we had to give descriptions to sub-patterns via syntax classes. Also, some related options are mutually exclusive, such as #:inspector, #:transparent, and #:prefab.

  (struct name:id super:maybe-super (field:field ...)
    (~or (~optional
           (~or (~seq #:inspector inspector:expr)
                (~seq (~and #:transparent transparent-kw))
                (~seq (~and #:prefab prefab-kw)))
           #:name "#:inspector, #:transparent, or #:prefab option")
         (~optional (~seq (~and #:mutable) mutable-kw)
                    #:name "#:mutable option")
         (~optional (~seq #:super super-expr:expr)
                    #:name "#:super option")
         (~optional (~seq #:auto-value auto:expr)
                    #:name "#:auto-value option")
         (~optional (~seq #:guard guard:expr)
                    #:name "#:guard option")
         (~seq #:property prop:expr prop-val:expr)
         (~optional (~seq #:constructor-name constructor-name:id)
                    #:name "#:constructor-name option")
         (~optional
           (~seq #:extra-constructor-name extra-constructor-name:id)
           #:name "#:extra-constructor-name option")
         (~optional (~seq (~and #:omit-define-syntaxes omit-def-stxs-kw))
                    #:name "#:omit-define-syntaxes option")
         (~optional (~seq (~and #:omit-define-values omit-def-vals-kw))
                    #:name "#:omit-define-values option"))
    ...)
Here we have grouped the three incompatible options together under a single ~optional constraint. That means that at most one of any of those options is allowed. We have given names to the optional clauses. See ~optional for other customization options.

Note that there are other constraints that we have not represented in the pattern. For example, #:prefab is also incompatible with both #:guard and #:property. Repetition constraints cannot express arbitrary incompatibility relations. The best way to handle such contraints is with a side condition using #:fail-when.