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
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.
(begin-for-syntax (define-syntax-class cond-clause #:attributes (condition result) ; Expr, Expr (pattern [condition:expr result:expr])))
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
;; (my-evcase1 Expr [Expr Expr] ...) : Expr
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.
;; EC1Clause ::= [Expr Expr] -- comparison value, result
;; (my-evcase1 Expr EC1Clause ...) : Expr
(define-syntax my-evcase1 (syntax-parser [(_ find:expr c:ec1-clause ...) #'(my-cond [(equal? find c.comparison) c.result] ...)]))
> (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
(define-syntax my-evcase1 (syntax-parser [(_ find:expr c:ec1-clause ...) #'(let ([tmp find]) (my-cond [(equal? tmp c.comparison) c.result] ...))]))
> (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 ...))]))
;; (my-evcase1* Expr EC1Clause ...) : Expr
;; (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.
; (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.