Methods & Generic Functions¶
Dylan methods correspond roughly to the functions found in C++. 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 () format-out("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 () => () format-out("Hello, world!"); end method;
There have been two changes. The function now officially returns no values 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 are all integers”.
Functions with variable numbers of parameters include the #rest keyword in 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. (It may be done 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.
Because Dylan methods can’t have “output” parameters, 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. 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;
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 (numbers :: <list>) => (out :: <list>) map(method(x) x * x end, numbers); end;
The map function takes each element of the list numbers 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 resemble bare methods but have names. They are declared within other methods, often as private utility routines.
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 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() format-out(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 references to local variables that are outside of the local function’s own scope are known as closures. In the above example, string-putter “closes over” (or captures the binding of) the variable named string.
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 type of the argument 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 values:
define generic display (thing :: <object>) => ()
Generic functions are explained in greater detail in the chapter on multiple dispatch.
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++, and they are always optional.
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;
The arguments following #key are keyword arguments. You could call this method in several ways:
print-records(recs); print-records(recs, lines-per-page: 65); print-records(recs, lines-per-page: 120, init-codes: "***42\n");
The first line calls the method without using any of the keyword arguments. The second line uses one of the keyword arguments and the third uses both. Note that the order of the keyword arguments does not matter.
With all three calls, the init-codes and lines-per-page variables are available in the body of the method, even though keyword arguments are omitted in two of the calls. When a keyword argument is omitted, it is given the default value specified in the method definition. Therefore, in the first call, the lines-per-page variable has the value 66, and in the first and second calls, the init-codes variable has the value "".
Programmers have quite a bit of flexibility in specifying keyword arguments.
- The default value specifier (e.g. the = 66 above) may be omitted, in which case #f is used.
- The type of the keyword argument may be specified or omitted, just as with regular arguments.
- The keyword name can be different from the variable name used in the body of the method—a handy tool for preventing name conflicts.
- The default value specifier can be a complex expression, and it can even use earlier parameters.
- The keyword arguments allowed or required by each method can be specified by the generic function. For more on this, see Parameter Lists and Generic Functions below.
The following method uses some of these features:
define method subseq (seq :: <sequence>, #key start :: <integer> = 0, end: _end :: <integer> = seq.size) assert(start <= _end, "start is after end"); ... end;
Firstly, the start: and end: keyword arguments are both specialized as <integer>. The caller can only supply integers for these parameters. Secondly, the start: keyword argument is associated with the start variable in the body of the method as usual, but because the Dylan language does not allow a variable named end, that keyword argument is instead associated with the _end variable. Finally, if the end: keyword argument were omitted, the value of the _end variable would be the size of the seq argument.
An argument list can also include #rest, which is used with a variable name:
define method format (format-string, #rest format-parameters) ... end method
Any extra arguments are passed to the body of the method as a <sequence> in the specified variable. For example, if the above method were called like so:
format("Today will be %s with a high of %d.", "cloudy", 52);
The format-parameters variable in the body of the method would have the value #["cloudy", 52].
Parameter Lists and Generic Functions¶
A generic function restricts the parameter lists of its methods, but methods can expand on the generic function’s parameter list if the generic function allows it. This section describes how that works. It is a little more advanced than rest of this introduction, so you may want to skip this section for now and refer back to it later.
We described the #key and #rest parameter list tokens above. The #key token may also be used by itself, e.g., define method foo (arg, #key). And there is a third parameter list token, #all-keys, that indicates that a method permits other keyword arguments than those listed. These features are only useful when working with a generic function and its family of methods. When used together, these tokens must appear in the order #rest, #key, #all-keys.
The table below shows the different kinds of parameter lists that a generic function can have, and what effect each has on the parameter lists of the methods that it contains.
Generic function’s parameter list Methods’ parameter lists #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
- Each method must have this element in its parameter list.
- Each method may have this element in its parameter list, but is not required to.
- No method may have this element in its parameter list.
- Each method effectively has #all-keys in its parameter list, even if it is not present.
This table shows the different kinds of parameter lists that a method can have, what the r variable 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 other keywords (x) — No No (x, #key) — If next method permits If next method permits (x, #key a, b) — Yes If next method permits (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 If next method permits If next method permits (x, #rest r, #key a, b) Keywords/values Yes If next method permits (x, #rest r, #key, #all-keys) Keywords/values Yes Yes (x, #rest r, #key a, b, #all-keys) Keywords/values Yes Yes
- Extra arguments:
- The local variable r is set to a <sequence> containing all the arguments passed to the method beyond the required arguments (i.e., the sequence will not contain x).
- 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.
- If next method permits:
- 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 generic draw (s :: <shape>, #key); define class <polygon> (<shape>) ... end; define class <triangle> (<polygon>) ... end; define class <ellipse> (<shape>) ... end; define class <circle> (<ellipse>) ... end; define method draw (s :: <polygon>, #key sides) ... end; define method draw (s :: <triangle>, #key) ... end; define method draw (s :: <ellipse>, #key) ... end; define method draw (s :: <circle>, #key radius) ... 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.