5.6 Definition of a generic function

We repeat the definitions of the methods for say-time-of-day and say-time-offset here:

define method say-time-of-day (time :: <time-of-day>) => ()
  let(hours, minutes) = decode-total-seconds(time);
  format-out
    ("%d:%s%d", hours, if (minutes < 10) "0" else "" end, minutes);
end method say-time-of-day;
define method say-time-offset (time :: <time-offset>) => ()
  let(hours, minutes) = decode-total-seconds(time);
  format-out("%s %d:%s%d",
             if (past?(time)) "minus" else "plus" end, 
             hours, 
             if (minutes < 10) "0" else "" end,
             minutes);
end method say-time-offset;

Now that decode-total-seconds has an applicable method for instances of <time-offset> and <time-of-day>, both these methods work correctly:

? say-time-of-day(*my-time-of-day*);
0:02
? say-time-of-day(*your-time-of-day*);
8:30
? say-time-offset(*my-time-offset*);
plus 15:20
? say-time-offset(*your-time-offset*);
minus 6:45

We have defined two methods: say-time-offset and say-time-of-day. A method defined with define method cannot exist without a generic function. When you define a method, and no generic function of that name exists, Dylan automatically creates a generic function. When we defined these two methods, there were no generic functions with those names defined, so Dylan created module variables named say-time-of-day and say-time-offset, created the generic functions, stored the generic functions in the module variables, and added the methods to the generic functions.

These two methods are logically related to each other, but have no explicit relationship in the code, other than in the similarity of their names. A cleaner approach is to abstract the concept of what these methods are trying to do — that is, to describe an object. To introduce this abstraction, we define a new generic function.

We use define generic to define the generic function explicitly:

// Given an object, print a description of the object
define generic say (any-object :: <object>) => ();

This generic function has a name: say. It receives one argument: the object to describe. That argument must be of the type <object>. All objects are of the type <object>, so this generic function does not restrict the type of its argument.

Our definition for the generic function say is similar to that of the generic function that Dylan would have created automatically if we had defined a method for say before we defined the generic function say. (The only difference is that the automatically defined generic function would have a more general value declaration.) However, defining the generic function explicitly enables us to formalize its purpose, to name the parameter, to specify a type constraint on the parameter, to specify the return values and their types, and to give comments about the generic function as a whole. The generic function defines the contract that all methods for this generic function must obey. The contract of the say generic function is as follows:

The say generic function receives one required argument, which must be of the type <object>. It prints a description of the object. The say generic function returns no values.

Dylan requires all the methods for a generic function to have congruent parameter lists and values declarations. See Section 12.2.5, page 176.

Now, we define two methods for say. The method for say on <time-of-day> fulfills the same purpose (and has the same body) as the say-time-of-day method, which we remove from the library with an editor or a gesture in the environment.

define method say (time :: <time-of-day>) => ()
  let (hours, minutes) = decode-total-seconds(time); 
  format-out
    ("%d:%s%d", hours, if (minutes < 10) "0" else "" end, minutes);
end method say;

Similarly, the method for say on <time-offset> is intended to replace say-time-offset, which we remove.

define method say (time :: <time-offset>) => ()
  let(hours, minutes) = decode-total-seconds(time);
  format-out("%s %d:%s%d",
             if (past?(time)) "minus" else "plus" end, 
             hours, 
             if (minutes < 10) "0" else "" end,
             minutes);
end method say;

Figure 5.5 shows that the generic function say has two methods defined for it.

Figure 5.5 Methods for the say generic function.
Generic function say
define method say (time :: <time-of-day>) => ()
  let (hours, minutes) = decode-total-seconds(time);
  format-out
    ("%d:%s%d", hours, if (minutes < 10) "0" else "" end, minutes);
end say;
define method say (time :: <time-offset>) => ()
  let (hours, minutes) = decode-total-seconds(time);
  format-out("%s %d:%s%d",
             if (past?(time)) "minus" else "plus" end,
             hours,
             if (minutes < 10) "0" else "" end,
             minutes);
end say;

We can call say:

? say(*my-time-of-day*);
0:02

In the preceding call, the argument is of the type <time-of-day>, so the method on <time-of-day> is the only applicable method. That method is invoked.

? say(*my-time-offset*);
plus 15:20

In the preceding call, the argument is of the type <time-offset>, so the method on <time-offset> is the only applicable method. That method is invoked.