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).
;; AttributesClause ::= #:attributes (Id ...) ;; WithClause ::= #:with SyntaxPattern Expr
(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])
;; CondClause ::= ;; | [Expr Expr] ;; | [Expr #:apply Expr] ;; | #:do Body
(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))))
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
;; (my-cond CondClause ... MaybeFinalCondClause) : Expr
;; MaybeFinalCondClause ::= ε | #:else Expr
(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?
(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])
;; CondClause[∅] ::= [Expr Expr] ;; CondClause[∅] ::= [Expr #:apply Expr] ;; CondClause[Δ] ::= #:do Body[Δ]
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{Γ,Δ}
;; (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.