On this page:
5.1 Defining New Shapes
5.2 Same Structure, Different Interpretation
5.3 Helper Macros and Simple Expressions

5 Shape Definitions

This section shows how to define new shapes and their corresponding syntax classes.

This section also introduces a shape for simple expressions.

5.1 Defining New Shapes

Consider the shape we’ve given to my-cond:

;; (my-cond [Expr Expr] ...) : Expr

This tells us the structure of my-cond’s arguments, but it gives us no hook upon which to hang a description of the arguments’ interpretation. Let’s give it a name:

;; CondClause ::= [Expr Expr]   -- represent condition, result

Now when we describe the behavior of my-cond, we can separate out the structure and interpretation of CondClauses from the discussion of my-cond itself.
  • The my-cond form takes a sequence of CondClauses, and that it tries each CondClause in order until one is selected, and the result of the my-cond expression is the result of the selected CondClause, or (void) if none was selected.

  • A CondClause consists of two expressions. The first represents a condition; if it evaluates to a true value, then the clause is selected. The second expression determines the clause’s result.

I typically write something like the terse comment above in the source code and include the longer, more precise version in the documentation.

We should also define a syntax class, cond-clause, corresponding to the new shape:
(begin-for-syntax
  (define-syntax-class cond-clause
    #:attributes (condition result) ; Expr, Expr
    (pattern [condition:expr result:expr])))
The syntax class has a single pattern form specifying the structure of the terms it accepts. The pattern variables are exported from the syntax class as syntax-valued attributes. I’ve written a comment after the #:attributes declaration with the shape of each attribute; they both contain Expr terms.

My convention is to use capitalized names such as Expr, Id, and CondClause for shapes and lower-case names such as expr, id, and cond-clause for syntax classes. Distinguishing them serves as a reminder that syntax classes represent some but not all of the meaning of shapes, just like Racket’s contracts capture some but not all of the meaning of types. The syntax class checks that terms have the right structure, and its attribute names hint at their intended interpretation, but the syntax class cannot enforce that interpretation.

We update the macro’s shape to refer to the new shape name, and we update the implementation’s pattern to use a pattern variable annotated with the new syntax class (c:cond-clause). In the template, we refer to the pattern variable’s attributes defined by the syntax class (c.condition and c.result).

; (my-cond CondClause ...) : Expr
(define-syntax my-cond
  (syntax-parser
    [(_)
     #'(void)]
    [(_ c:cond-clause more ...)
     #'(if c.condition
           c.result
           (my-cond more ...))]))

In addition to improved organization, another benefit of defining cond-clause as a syntax class is that my-cond now automatically uses cond-clause to help explain syntax errors. For example:

> (my-cond 5)

eval:9:0: my-cond: expected cond-clause

  at: 5

  in: (my-cond 5)

> (my-cond [#t #:whoops])

eval:10:0: my-cond: expected expression

  at: #:whoops

  in: (my-cond (#t #:whoops))

  parsing context:

   while parsing cond-clause

    term: (#t #:whoops)

    location: eval:10:0

In the implementation above, should we also annotate more to check that all of the arguments are clauses, instead of only checking the first clause at each step? That is:

; (my-cond CondClause ...) : Expr
(define-syntax my-cond
  (syntax-parser
    [(_)
     #'(void)]
    [(_ c:cond-clause more:cond-clause ...)
     #'(if c.condition
           c.result
           (my-cond more ...))]))

It can lead to earlier detection of syntax errors and better error messages, because the error is reported in terms of the original expression the user wrote, as opposed to one created by the macro for recursion. The cost is that the syntax-class check is performed again and again on later arguments; the number of cond-clause checks performed by this version is quadratic in the number of clauses it originally receives. One solution is to make the public my-cond macro check all of the clauses and then expand into a private recursive helper macro that only interprets one clause at a time.

; (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)]
    [(_ c:cond-clause more ...)
     #'(if c.condition
           c.result
           (my-cond* more ...))]))

This tension arises because a syntax class has two purposes: validation and interpretation. For a single term, validation should precede interpretation, but if a macro has many arguments (for example, a use of my-cond might have many CondClauses), how should we interleave validation and interpretation of the many terms? One appealing goal is to validate all arguments before interpreting any of them. Another appealing goal is to only “call” a syntax class once per term. Each goal constrains the ways we can define the syntax class and write the macro; achieving both goals is especially tricky.

A related question is this: How much of the task of interpeting a term belongs to the syntax class versus the macro that uses it? The division of responsibility between syntax class and macro affects the interface between them, and that interface affects how the macro is written. This question becomes more complicated when we add variants to a syntax class; we discuss the difficulties and solutions in detail in Enumerated Shapes.

5.2 Same Structure, Different Interpretation

Recall Exercise 9. The goal was to design a macro my-evcase1 with the following shape:
;; (my-evcase1 Expr [Expr Expr] ...) : Expr
The exercise’s description of the macro’s behavior referred to “clauses”, which is a hint that we should improve the specification by naming that argument shape. Let’s do that now.

We already have a name for the shape [Expr Expr]; should we simply define the shape of my-evcase1 in terms of CondClause? (Perhaps we should also generalize the name to ClauseWith2Exprs so it doesn’t seem so tied to my-cond?)

No. The structure of the two shapes is the same, but the interpretation is different. Specifically, the first expression of a CondClause is treated as a condition, but the first expression of a my-evcase1 clause is treated as a value for equality comparison. Furthermore, the two macros happen to have the same clause structure now, but if we add features to one or the other (and we will), they might evolve in different ways. In fact, they are likely to evolve in different ways because they have different interpretations.

So let’s define a new shape, EC1Clause, for my-evcase1 clauses:
;; EC1Clause ::= [Expr Expr]    -- comparison value, result
Here is the corresponding syntax class:

Now the macro has the shape
;; (my-evcase1 Expr EC1Clause ...) : Expr

One implementation strategy is to use my-cond as a helper macro. Here’s a first attempt that isn’t quite right:
(define-syntax my-evcase1
  (syntax-parser
    [(_ find:expr c:ec1-clause ...)
     #'(my-cond [(equal? find c.comparison) c.result] ...)]))
These examples illustrate the problem:
> (my-evcase1 (begin (printf "got a coin!\n") (* 5 5))
    [5 "nickel"] [10 "dime"] [25 "quarter"] [(/ 0) "infinite money!"])

got a coin!

got a coin!

got a coin!

"quarter"

> (define coins '(25 5 10 5))
> (define (get-coin) (begin0 (car coins) (set! coins (cdr coins))))
> (my-evcase1 (get-coin)
    [5 "nickel"] [10 "dime"] [25 "quarter"] [(/ 0) "infinite money!"])

/: division by zero

The initial expression is re-evaluated for every comparison, which is problematic if the expression has side-effects.

Here is a fixed implementation that uses a temporary variable to hold the value of the first expression:
(define-syntax my-evcase1
  (syntax-parser
    [(_ find:expr c:ec1-clause ...)
     #'(let ([tmp find])
         (my-cond [(equal? tmp c.comparison) c.result] ...))]))

Now the examples behave as expected:
> (my-evcase1 (begin (printf "got a coin!\n") (* 5 5))
    [5 "nickel"] [10 "dime"] [25 "quarter"] [(/ 0) "infinite money!"])

got a coin!

"quarter"

> (define coins '(25 5 10 5))
> (define (get-coin) (begin0 (car coins) (set! coins (cdr coins))))
> (my-evcase1 (get-coin)
    [5 "nickel"] [10 "dime"] [25 "quarter"] [(/ 0) "infinite money!"])

"quarter"

Exercise 10: Turn the examples above into test cases for my-evcase1. Check that the tests fail on the original version of the macro and succeed on the fixed version.

The catch-output macro from Exercise 3 and Proper Lexical Scoping, Part 2 is not quite enough to express these tests conveniently. Write a more general helper macro with the following shape:
;; (result+output Expr[R]) : Expr[(list R String)]
and use that to express your tests.

5.3 Helper Macros and Simple Expressions

Recall the implementation strategies for handling ellipsis shapes from Ellipses with Simple Shapes. The first strategy was to write a recursive macro. Is it possible to implement my-evcase1 using that strategy?

No. It is not possible to implement my-evcase1 as a recursive macro, according to the shape we’ve given it, while guaranteeing that we evaluate the initial expression once. Compare this with fact that some list functions cannot be written purely as structural recursive functions. The average function is a good example: it can only be expressed by combining or adjusting the results of one or more structurally recursive helper functions.

We can, however, implement my-evcase1 using a recursive helper macro. In fact, we’ve done that in the previous implementation, by using my-cond. But let’s try a different implementation using a recursive macro that has a shape that is similar to, although not identical to, that of my-evcase1. In particular, it is worth talking about the shape involved in the interface between the main, public macro and its private helper.

; (my-evcase1 Expr EC1Clause ...) : Expr
(define-syntax my-evcase1
  (syntax-parser
    [(_ find:expr c:ec1-clause ...)
     #'(let ([tmp find])
         (my-evcase1* tmp c ...))]))
So, what is the shape of the helper macro, my-evcase1*?

We could describe my-evcase1* with the following shape:
;; (my-evcase1* Expr EC1Clause ...) : Expr
but that’s the same shape as my-evcase1, so if we’re using the shapes to guide our design — specifically, our implementation options — we have not made any progress. The point of my-evcase1* is that its first argument is a simple variable reference, not any arbitrary expression whose evaluation might be costly or involve side effects. Let’s reflect that in the shape:
;; (my-evcase1* SimpleExpr EC1Clause ...) : Expr

The SimpleExpr shape is like Expr, except that it only contains expressions that we are willing to duplicate. That is, the expansion of the expression is simple and small, and the evaluation of the expression is trivial and does not involve side effects. Acceptable expressions include quoted constants and variable references. Usually, we also expect simple expressions to be constant, so a variable reference should be to a fresh local variable that is never mutated. Depending on the situation, there might be other expressions that we would accept as simple.

There is no separate syntax class for SimpleExpr; just use expr or omit the syntax class annotation. It is infeasible to check whether an expression is simple; instead, you should only make private macros accept SimpleExpr arguments, and you should check that all of the public macros that call them pass appropriate expressions.

In this example, let’s assume that my-evcase1* is private and only my-evcase calls it. The initial expression that my-evcase gives to the helper is a local variable reference, which is simple.

Here is a recursive implementation of my-evcase1*:
; (my-evcase1* SimpleExpr EC1Clause ...) : Expr
(define-syntax my-evcase1*
  (syntax-parser
    [(_ tmp)
     #'(void)]
    [(_ tmp c:ec1-clause more ...)
     #'(if (equal? tmp c.comparison)
           c.result
           (my-evcase1* tmp more ...))]))

Note that the second clause duplicates the tmp argument.