21.5 More complex rules

The macros shown so far have all been simple: a single pattern transformed into a single template. To get a flavor of the full power of the Dylan macro system, consider this defining macro:

define macro aircraft-definer
  { define aircraft ?identifier:name (?type:name) ?flights end }
   => { register-aircraft(make("<" ## ?type ## ">", id: ?#"identifier"));
        register-flights(?#"identifier", ?flights) }
flights:
  { }
   => { }
  { ?flight; ... }
   => { ?flight, ... }
flight:
  { flight ?id:name, #rest ?options:expression }
   => { make(<flight>, id: ?#"id", ?options) }
end macro aircraft-definer;

We might use the macro define aircraft as follows:

define aircraft UA4906H (DC10)
  flight UA11, from: #"BOS", to: #"SFO";
  flight UA12, from: #"SFO", to: #"BOS";
end aircraft UA4906H;

This macro shows a number of the more esoteric features of Dylan macros. First, notice the pattern variable ?flights, which has no constraint, but rather is called out as an auxiliary rule. When the compiler matches this macro, it will try each of the auxiliary rule's patterns listed under flights: for a match. When it finds a match, it will assign the pattern variable ?flights to the fragment resulting from the matching pattern's template substitution. In effect, auxiliary rules give a way of writing new constraints, combined with the effect of a subroutine for matching and substitution.

In this particular case, we use the auxiliary rule to map yet another auxiliary rule, flight, over a sequence of flight descriptions that look similar to the slot descriptions in a class. The mapping is signaled by the points of ellipsis (...) which means that the rule should be applied recursively (that is, the current rule is matched again to the fragment that matches ...). Note that flights must have a rule to cover the case of there being no flight; that rule also handles the end of the recursion when the final flight has been matched.

The flight rule simply converts each flight name and its options into the appropriate call to make, to create the flight. We could extend this rule to allow a more natural specification for flight origin, destination, and time.

We do the work of defining an aircraft by calling the helper functions register-aircraft and register-flights (which are not given here), but the macro takes care of getting the arguments in order. The substitution "<" ## ?type ## ">" turns the name DC10 into the name <DC10> by using concatenation, allowing a more concise format for our definer while maintaining our convention for naming types. The substitution ?#"identifier" turns the name UA1306 into the symbol #"UA1306" by using coercion; the program can use the symbol #"UA1306" to look up an aircraft in the registry by name. The template for flights collects all the individual flights into a comma-separated list that is passed to register-flights as a #rest argument.