Methods & Generic Functions

Dylan methods correspond roughly to the functions found in C and Pascal. They take zero or more named parameters, but also return zero or more named return values. A minimal Dylan method might look like the following:

define method hello-world ()
  puts("Hello, world!");
end;

This method has no parameters and an unspecified return value. It could return any number of values of any type. In order to make the above code more clear, the function could be rewritten as follows:

define method hello-world () => ();
  puts("Hello, world!");
end method;

There have been two changes. The function now officially returns no value whatsoever. Also note that end has been replaced by end method which could in turn be rewritten as end method hello-world. In general, Dylan permits all the obvious combinations of keywords and labels to follow an end statement.

Parameters & Parameter Lists

Dylan methods declare parameters in fashion similar to that of conventional languages, except for the fact that parameters may optionally be untyped. Both of the following methods are legal:

define method foo (x :: <integer>, y) end;
define method bar (m, s :: <string>) end;

Both foo and bar have one typed and one untyped parameter, but neither has a well-defined return value (or actually does anything). As in C, each typed parameter must have its own type declaration; there’s no syntax for saying “the last three parameters were all integers”.

Functions with variable numbers of parameters include the #rest keyword at the end of their parameter lists. Thus, the declaration for C’s printf function would appear something like the following in Dylan:

define method printf (format-string :: <string>, #rest arguments) => ();
  // Print the format string, extracting one at a time from "arguments".
  // Note that Dylan actually allows us to verify the types of variables,
  // preventing those nasty printf errors, such as using %d instead of %ld.
  // ...
end method printf;

Note that Dylan makes no provision for passing variables by reference in the Pascal sense, or for passing pointers to variables. parameter names are simply bound to whatever values are passed, and may be rebound like regular variables. This means that there’s no way to write a swap function in Dylan (except by using macros). However, the following function works just fine, because it modifies the internal state of another object:

define method sell (car :: <car>, new-owner :: <string>) => ();
  if (credit-check(new-owner))
    car.owner := new-owner;
  else
    error("Bad credit!");
  end;
end;

If this sounds unclear, reread the chapter on variables and expressions.

Return Values

Because Dylan methods can’t have normal “output” parameters in their parameter lists, they’re allowed considerably more flexibility when it comes to return values. Methods may return more than one value. As with parameters, these values may be typed or untyped. Interestingly enough, all return values must be named.

A Dylan method – or any other control construct – returns the value of the last expression in its body.

define method foo () => sample :: <string>;
  "Sample string.";  // return string
end;

define method bar () => my-untyped-value;
 if (weekend-day?(today()))
    "Let's party!";  // return string
 else
    make(<excuse>);  // return object
  end if;
end method;

define method moby ()
  =>sample :: <string>, my-untyped-value;
  values(foo(), bar());  // return both!
end;

define method baz () => ();
  let (x,y) = moby();  // assign both
end;

Bare Methods

Nameless methods may be declared inline. Such bare methods are typically used as parameters to other methods. For example, the following code fragment squares each element of a list using the built in map function and a bare method:

define method square-list (in :: <list>)
 => out :: <list>
  map(method(x) x * x end, in);
end;

The map function takes each element of the list in and applies the anonymous method. It then builds a new list using the resulting values and returns it. The method square-list might be invoked as follows:

square-list(#(1, 2, 3, 4));
=> #(1, 4, 9, 16)

Local Methods

Local methods resemble bare methods but have names. They are declared within other methods, often as private utility routines. Local methods are typically used in a fashion similar to Pascal’s local functions.

define method sum-squares (in :: <list>) => sum-of-element-squares :: <integer>;
  local method square (x)
          x * x;
        end,
        method sum (list :: <list>)
          reduce1(\+, list);
        end;
  sum(map(square, in));
end;

Local methods can actually outlive the invocation of the function which created them. parameters of the parent function remain bound in a local method, allowing some interesting techniques:

define method build-put (string :: <string>) => (res :: <function>);
  local method string-putter()
          puts(string);
        end;
  string-putter;  // return local method
end;

define method print-hello () => ();
  let f = build-put("Hello!");
  f();  // print "Hello!"
end;

Local functions which contain bound variables in the above fashion are known as closures.

Generic functions

A generic function represents zero or more similar methods. Every method created by means of define method is automatically contained within the generic function of the same name. For example, a programmer could define three methods named display, each of which acted on a different data type:

define method display (i :: <integer>)
  do-display-integer(i);
end;

define method display (s :: <string>)
  do-display-string(s);
end;

define method display (f :: <float>)
  do-display-float(f);
end;

When a program calls display, Dylan examines all three methods. Depending on the number and type of arguments to display, Dylan invokes one of the above methods. If no methods match the actual parameters, an error occurs.

In C++, this process occurs only at compile time. (It’s called operator overloading.) In Dylan, calls to display may be resolved either at compile time or while the program is actually executing. This makes it possible to define methods like:

define method display (c :: <collection>)
  for (item in c)
    display(item);  // runtime dispatch
  end;
end;

This method extracts objects of unknown type from a collection, and attempts to invoke the generic function display on each of them. Since there’s no way for the compiler to know what type of objects the collection actually contains, it must generate code to identify and invoke the proper method at runtime. If no applicable method can be found, the Dylan runtime environment throws an exception.

Generic functions may also be declared explicitly, allowing the programmer to exercise control over what sort of methods get added. For example, the following declaration limits all display methods to single parameter and no return value:

define generic display (thing :: <object>) => ()

Generic functions are explained in greater detail in the chapter on multiple dispatch.

Keyword Arguments

Functions may accept keyword arguments, extra parameters which are identified by a label rather than by their position in the argument list. Keyword arguments are often used in a fashion similar to default parameter values in C++. For example, the following hypothetical method might print records to an output device:

define method print-records
 (records :: <collection>, #key init-codes = "", lines-per-page = 66)
 => ();
  send-init-codes(init-codes);
  // ...print the records
end method;

This method could be invoked in one of several ways. The first specifies no keyword arguments, and the latter two specify some combination of them. Note that order of keyword arguments doesn’t matter.

print-records(recs);
print-records(recs, lines-per-page: 65);
print-records(recs, lines-per-page: 120, init-codes: "***42\n");

Programmers have quite a bit of flexibility in specifying keyword arguments. They may optionally omit the default value for a keyword (in which case #f is used). Default value specifiers may actually be function calls themselves, and may rely on regular parameters already being in scope. Variable names may be different from keyword names, a handy tool for preventing name conflicts.

A generic function can restrict the parameter lists of its methods. This table shows the different kinds of parameter lists that a generic function can have, and what effects they have on the parameter lists of its methods.

Generic function’s parameter list #key #key a, b #all-keys #rest
(x) Forbidden Forbidden Forbidden Forbidden
(x, #key) Required Allowed Allowed Allowed
(x, #key a, b) Required Required Allowed Allowed
(x, #key, #all-keys) Required Allowed Automatic Allowed
(x, #key a, b, #all-keys) Required Required Automatic Allowed
(x, #rest r) Forbidden Forbidden Forbidden Required
Automatic
Every method effectively has #all-keys in its parameter list.

A method can expand on the keyword parameters specified by its generic function. This table shows the different kinds of parameter lists that a method can have, what the r argument contains for each, and which keywords are permitted by each. It is a run-time error to call a method with a keyword argument that it does not permit.

Method’s parameter list Contents of r Permits a: and b: Permits c:
(x) No No
(x, #key) Next method Next method
(x, #key a, b) Yes Next method
(x, #key, #all-keys) Yes Yes
(x, #key a, b, #all-keys) Yes Yes
(x, #rest r) Extra arguments No No
(x, #rest r, #key) Keywords/values Next method Next method
(x, #rest r, #key a, b) Keywords/values Yes Next method
(x, #rest r, #key, #all-keys) Keywords/values Yes Yes
(x, #rest r, #key a, b, #all-keys) Keywords/values Yes Yes
Keywords/values
The local variable r is set to a <sequence> containing all the keywords and values passed to the method. The first element of the sequence is one of the keywords, the second is the corresponding value, the third is another keyword, the fourth is its corresponding value, etc.
Next method
The method only permits a keyword if some other applicable method permits it. In other words, it permits all the keywords in the next-method chain, effectively inheriting them. This rule is handy when you want to allow for future keywords that make sense within a particular family of related classes but you do not want to be overly permissive.

To illustrate the “next method” rule, say we have the following definitions:

define class <shape> (<object>) ... end;
define class <polygon> (<shape>) ... end;
define class <ellipse> (<shape>) ... end;

define class <circle> (<ellipse>) ... end;
define class <triangle> (<polygon>) ... end;

define generic draw (s :: <shape>, #key);

define method draw (s :: <circle>, #key radius) ... end;
define method draw (s :: <polygon>, #key sides) ... end;
define method draw (s :: <triangle>, #key) ... end;

The draw methods for <polygon> and <triangle> permit the sides: keyword. The method for <triangle> permits sides: because the method for <polygon> objects also applies to <triangle> objects and that method permits sides:.

However, the draw method for <circle> only permits the radius: keyword, because the draw method for <polygon> does not apply to <circle> objects — the two classes branch off separately from <shape>.

Finally, the method for <ellipse> does not permit the radius: keyword because, while a circle is a kind of ellipse, an ellipse is not a kind of circle. <circle> does not inherit from <ellipse> and the draw method for <circle> objects does not apply to <ellipse> objects.

For more information on keyword arguments, especially their use with generic functions, see the DRM.