On this page:
7.1 Shapes for Multiple Terms
7.1.1 Redo Case Analysis
7.1.2 Code Generator
7.1.3 AST
7.2 Optional Shapes
7.3 Shapes, Types, and Scopes (★)

7 Multi-Term Shapes

This section introduces “multi-term” shapes, used to describe syntactic elements like keyword arguments.

7.1 Shapes for Multiple Terms

In Racket, the syntax of a “keyword argument” to a function does not consist of a single term; it consists of two terms, a keyword followed by the argument term. That is, the logical grouping structure does not correspond with the term structure. The syntax of macros generally follows the same idiom: a macro keyword argument consists of the keyword and zero or more argument terms, depending on the keyword. For example, the #:attributes keyword used by define-syntax-class takes one argument (a list of attributes); and the #:with keyword takes two (a syntax pattern and an expression).

We can define shapes that stand for multiple terms like this:
;; AttributesClause ::= #:attributes (Id ...)
;; WithClause       ::= #:with SyntaxPattern Expr
Multi-term shapes are represented by splicing syntax classes, which encapsulate head syntax patterns (so called because they match some variable-length “head” of the list term).

Let’s extend my-cond with support for a #:do clause that has a single Body argument. That will allow us to include definitions between tests. Here’s an example:
(define ls '((a 1) (b 2) (c 3)))
(define entry (assoc 'b ls))
(my-cond [(not entry) (error "not found")]
         #:do (define value (cadr entry))
         [(even? value) 'even]
         [(odd? value) 'odd])

Here is the revised definition of CondClause, which is now a multi-term shape:
;; CondClause ::=
;; | [Expr Expr]
;; | [Expr #:apply Expr]
;; | #:do Body

Here is the corresponding syntax class, including only the patterns:
(begin-for-syntax
  (define-splicing-syntax-class cond-clause
    #:attributes ()
    (pattern [condition:expr result:expr])
    (pattern [condition:expr #:apply get-result:expr])
    (pattern (~seq #:do body:expr))))
We must declare my-cond using define-splicing-syntax-class, and we must use ~seq to wrap multiple-term patterns.

What interface can we give to the syntax class, and how do we implement the macro? Let’s review the implementations of my-cond from Defining Enumerated Shapes:
  • The approach of redoing the case analysis from Empty Interface Strategy (Redo Case Analysis) would also still work.

  • The approach from Common Meaning Interface Strategy no longer works, because #:do clauses are not a special case of #:apply clauses.

  • The failure-continuation approach from Macro Behavior Interface Strategy no longer works, because the scope of definitions within #:do clauses should cover the rest of the clauses, but the failure continuation is received as a closure value, and there’s no way to affect its environment.

  • The code generator approach from Code Generator Interface Strategy would still work, since the code generator for the #:do clause can put the expression representing the rest of the clauses in the scope of the #:do-clause’s definitions.

  • The AST approach from AST Interface Strategy would still work. We would need to update the AST datatype with a new variant and update the macro’s case analysis to handle it.

This is a good summary of how robust each of these strategies is to changes in the shape.

7.1.1 Redo Case Analysis

For the empty interface, we simply add a case to the private, recursive macro:

(begin-for-syntax
  (define-splicing-syntax-class cond-clause
    #:attributes ()
    (pattern [condition:expr result:expr])
    (pattern [condition:expr #:apply get-result:expr])
    (pattern (~seq #:do body:expr))))
; (my-cond CondClause ...) : Expr
(define-syntax my-cond
  (syntax-parser
    [(_ c:cond-clause ...)
     #'(my-cond* c ...)]))
; (my-cond* CondClause ...) : Expr
(define-syntax my-cond*
  (syntax-parser
    [(_)
     #'(void)]
    [(_ [condition:expr result:expr] more ...)
     #'(if condition
           result
           (my-cond* more ...))]
    [(_ [condition:expr #:apply get-result:expr] more ...)
     #'(let ([condition-value condition])
         (if condition-value
             (get-result condition-value)
             (my-cond* more ...)))]
    [(_ #:do body:expr more ...)
     #'(let ()
         body
         (my-cond* more ...))]))
7.1.2 Code Generator

With the code generator strategy, the new implementation simply involves two changes to the old implementation. We must change define-syntax-class to define-splicing-syntax-class, and we must add the third variant as below. The definition of my-clause itself does not change.

(begin-for-syntax
  (define-splicing-syntax-class cond-clause
    #:attributes (make-code) ; Syntax[Expr] -> Syntax[Expr]
    (pattern [condition:expr result:expr]
             #:attr make-code (lambda (fail-expr)
                                #`(if condition result #,fail-expr)))
    (pattern [condition:expr #:apply get-result:expr]
             #:attr make-code (lambda (fail-expr)
                                #`(let ([condition-value condition])
                                    (if condition-value
                                        (get-result condition-value)
                                        #,fail-expr))))
    (pattern (~seq #:do body:expr)
             #:attr make-code (lambda (fail-expr)
                                #`(let ()
                                    body
                                    #,fail-expr)))))
; (my-cond CondClause ...) : Expr
(define-syntax my-cond
  (syntax-parser
    [(_ c:cond-clause ...)
     (foldr (lambda (make-code rec-expr)
              (make-code rec-expr))
            #'(void)
            (datum (c.make-code ...)))]))
7.1.3 AST

Exercise 16: Adapt the solution from AST Interface Strategy to support #:do-clauses.

7.2 Optional Shapes

A common kind of multi-term shape is one that has two (or more variants), one of which consists of zero terms. A good naming convention for such shapes and syntax classes is to start them with “maybe” or “optional”. For example, we could add an optional final #:else clause to my-cond, like this:
;; (my-cond CondClause ... MaybeFinalCondClause) : Expr
where MaybeFinalCondClause is defined as follows:
;; MaybeFinalCondClause ::= ε | #:else Expr
Here I’ve used ε to represent zero terms.

The corresponding syntax class for MaybeFinalCondClause must be a splicing syntax class. The interpretation of the possible final clause is that it provides a condition-free result if none of the previous clauses were selected; if absent, the result is (void). So we can represent the interpretation with a single attribute holding an expression:
(begin-for-syntax
  (define-splicing-syntax-class maybe-final-cond-clause
    #:attributes (result) ; Expr
    (pattern (~seq)
             #:with result #'(void))
    (pattern (~seq #:else result:expr))))

Here is the macro, starting from the code-generation implementation above. The only changes are to the pattern and the use of #'fc.result instead of #'(void) in the call to foldr.

(define-syntax my-cond
  (syntax-parser
    [(_ c:cond-clause ... fc:maybe-final-cond-clause)
     (foldr (lambda (make-code rec-expr)
              (make-code rec-expr))
            #'fc.result
            (datum (c.make-code ...)))]))

7.3 Shapes, Types, and Scopes (★)

In The Id (Identifier) Shape and Expressions, Types, and Contracts we explored how to express scoping and type relationships between parts of a shape. Can we extend the notation to express the scoping of the #:do form of CondClause?

Recall the example program:
(define ls '((a 1) (b 2) (c 3)))  ; (Listof (list Symbol Integer))
(define entry (assoc 'b ls))      ; (list Symbol Integer)
(my-cond [(not entry) (error "not found")]
         #:do (define value (cadr entry))
         [(even? value) 'even]
         [(odd? value) 'odd])

How does each clause affect the environment of subsequent clauses? The first clause has no effect on the environments of the following clauses. The second clause adds a variable binding value with type Integer. More generally, since we could define multiple variables using define-values or combine definitions with begin, each clause might bind a set of variables, and each variable has a corresponding type. The first clause produces no bindings (so , the empty set); the second set produces {value:Integer}; the third and fourth clauses also produce . Let’s add a parameter to CondClause representing the bindings it “produces” — that is, the bindings it adds to the environments of subsequent clauses. We need to change the way we write the shape definition:
;; CondClause[∅] ::= [Expr Expr]
;; CondClause[∅] ::= [Expr #:apply Expr]
;; CondClause[Δ] ::= #:do Body[Δ]
We need the same information from the Body shape. Note that Δ does not stand for a type; it stands for a set of pairs of names and types — that is, a fragment of a type environment.

In the second clause of this example, Δ is {value:Integer}. That is:

#:do (define value (cadr entry))   : CondClause[{value:Integer}]

because

(define value (cadr entry))        : Body[{value:Integer}]

We also need to change the way we talk about lists of clauses. The notation CondClause ... doesn’t give us a good way to talk about the relationship between different clauses. Instead, let’s define a multi-term shape called CondClauses:

;; CondClauses ::= ε
;; CondClauses ::= CondClause[Δ] CondClauses{Δ}

By CondClauses{Δ} I mean that all expressions, body terms, etc within the clause are in the scope of the additional bindings described by Δ. That is, an environment annotation is implicitly propagated to all of a shape’s sub-shapes. The second line says that in CondClauses sequence, if one clause produces some bindings, then subsequent clauses are in their scope.

Here are the shape definitions with the environment annotations made fully explicit, where Γ stands for a type environment:

;; CondClause{Γ}[∅] ::= [Expr{Γ} Expr{Γ}]
;; CondClause{Γ}[∅] ::= [Expr{Γ} #:apply Expr{Γ}]
;; CondClause{Γ}[Δ] ::= #:do Body{Γ}[Δ]
 
;; CondClauses{Γ} ::= ε
;; CondClauses{Γ} ::= CondClause[Δ] CondClauses{Γ,Δ}

Finally, here is the shape of my-cond:
;; (my-cond CondClauses)    : Expr      -- implicit
;; (my-cond CondClauses{Γ}) : Expr{Γ}   -- explicit

That shows how to use the shape notation to specify type and scoping relationships between components of shapes.

For many macros, it is probably unnecessary to put this level of detail in the shape declarations. A more practical approach might be to limit the shapes to specifying syntactic structure and interpretation, as we’ve been doing, and describe the scoping of shapes and macros in prose.

On the other hand, a more precise specification is sometimes useful when writing macros with complicated binding structures. So keep this tool in mind in case you need it.