21.4 Constraints

So far, in our macros, we have seen the constraint expression used for the pattern variables. Except for a few unusual cases, pattern variables must always have a constraint associated with them. Constraints serve two purposes: they limit the fragment that the pattern variable will match, and they define the meaning of the pattern variable when it is substituted. As an example, consider the following statement macro, which we might find useful for manipulating the decoded parts of seconds:

define macro with-decoded-seconds
  { 
    with-decoded-seconds 
        (?max:variable, ?min:variable, ?sec:variable = ?time:expression) 
      ?:body 
    end
  }
 => {
       let (?max, ?min, ?sec) = decode-total-seconds(?time);
       ?body
    }
end macro;

The preceding macro might be used as follows:

define method say (time :: <time>)
  with-decoded-seconds(hours, minutes, seconds = time)
    format-out("%d:%s%d", 
               hours, if (minutes < 10) "0" else "" end, minutes);
  end;
end method say;

A statement macro can appear anywhere that a begin/end; block can appear. A statement macro introduces a new begin word — in this case, with-decoded-seconds — and is matched against a fragment that extends up to the matching end.

The pattern and the constraints on the pattern variables limit what the macro will match; they define the syntax of this particular statement. In the case of with-decoded-seconds, the syntax of this statement begins with a parenthesized list of

After the parenthesized list comes a body (any sequence of expressions separated by ;, just as would be valid in a begin/end; block). Note the use of the abbreviation ?:body, to mean ?body:body (a pattern variable, body, with the constraint body).

The constraints are similar to type declarations on variables: They limit the acceptable values of the pattern variables, and they help to document the interface of the macro. The constraints also serve a second purpose: Once the compiler has recognized a fragment under a particular constraint, it will ensure the correct behavior of that fragment when that fragment is substituted in a template. For example, suppose that we define a function macro:

define macro times
  { times (?arg1:expression, ?arg2:expression ) } =>
    { ?arg1 * ?arg2 }
end macro times;

We might use the macro as follows:

times(1 + 3, 2 + 5);

Here is the expanded macro:

1 + 3 * 2 + 5

We can see that, if the macro were a simple text-substitution macro, the result would be 12, rather than the 28 we were expecting. But because, in Dylan, the constraint is maintained when a pattern variable is substituted (that is, the expression that makes up each of the pattern variables remains a single expression), the result is as though the macro automatically inserted parentheses, and the expansion were

(1 + 3) * (2 + 5)

Some development environments may display the implicit parentheses of an expression constraint. Thus, the macro will yield the expected result of 28.

Comparison with C: Because C macros are simple textual substitutions, the macro writer must be sure to insert parentheses around every macro variable when it is substituted, and around the macro expansion itself, to prevent the resulting expansion from taking on new meanings.