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 a fashion similar to that of many 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 the #rest
keyword in their parameter list accept a variable
number of arguments. 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.
Return Values#
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;
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 (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#
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
.
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>)
write(*standard-output*, integer-to-string(i));
end;
define method display (s :: <string>)
write(*standard-output*, s);
end;
define method display (f :: <float>)
write(*standard-output*, float-to-string(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 method matches 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 are added.
For example, the following declaration limits all display
methods to a single parameter and no return values:
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++, 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.
Rest Arguments#
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
- Required:
Each method must have this element in its parameter list.
- Allowed:
Each method may have this element in its parameter list, but is not required to.
- Forbidden:
No method may have this element in its parameter list.
- Automatic:
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:
andb:
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 containx
).- 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.- 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.