User-Defined Classes and Methods¶
In this chapter, we show the most basic techniques for writing object-oriented code in Dylan. We define a class, make instances of the class, initialize slots of the instances, and get and set the values of slots. We define methods, and call them on the instances. One method returns multiple values — and that is an extremely useful technique. Another method uses local variables.
In this chapter, we start to develop an example of a library that represents different kinds of time. A library is a complete unit of code that can be used by many different clients, where a client can be another library or an application program. In Chapters Class Inheritance and Multimethods, we expand and refine the example that we begin in this chapter. A Simple Library, shows the result: a complete and working library.
Requirements of the time and position classes and methods¶
Our eventual goal in this book is to develop a sample application based on an airport theme. The sample application handles the scheduling of aircraft that are arriving into and departing from an airport. For more information, see Design of the Airport Application.
We know that, for our airport application, we need to represent time.
There are several ways to represent time. We could say that an event
happened 2 hours ago (a time offset). We could say that an event
happened at 21:30 (a time of day). We must represent both kinds of time
in our time library, and we must provide a way to print representations
of both. In this chapter, we define a class named <time-of-day>
, and
we define a method that prints a representation of <time-of-day>
. In
Class Inheritance, we define the <time-offset>
class, and a
method that prints a representation of <time-offset>
.
The airport application also requires us to represent physical objects (such as aircraft), and the positions (locations) of physical objects. In Modularity, we define classes that represent physical objects and positions.
Eventually, we need to be able to add times, to compare times for similarity, and to determine which of two times is greater than the other. We implement those operations in Multimethods.
We package the result of all our work into a complete and working library, in A Simple Library. Later, we refine this library to achieve greater modularity and extensibility. The final result is given in Four Complete Libraries.
User-defined classes¶
A user-defined class is like a structure or a record type in other languages. When you define a class, you specify its name, its direct superclasses, and its slots. A slot has a name and a type. Normally, each instance stores its own value for the slot. A class inherits the slots defined by its superclasses, and it can define more slots if it needs them.
The <time-of-day>
class¶
We start by defining a class to represent the concept of a time of day,
such as 21:30. The definition of the <time-of-day>
class is as
follows:
// A specific time of day from 00:00 (midnight) to below 24:00 (tomorrow)
define class <time-of-day> (<object>) // 1
slot total-seconds :: <integer>; // 2
end class <time-of-day>; // 3
The top line is a comment. The //
characters begin a comment, which
continues to the end of the line. We also provide comments that number
the lines of code after the first comment. The line numbers are useful
only for discussing the code examples in the book, and would not be used
in source files. You can also have multi-line comments that start with
/*
and end with */
.
On line 1, the words define class
start the class definition. The name
of the class is <time-of-day>
. The list following the name of the
class is a list of the direct superclasses of this class. The
<time-of-day>
class has one direct superclass, which is the class
<object>
. Each user-defined class must have at least one direct
superclass. If no other class is appropriate, the class must have
<object>
as its superclass.
Line 2 contains the only slot definition of this class. This class has
one slot, named total-seconds
. The slot’s type constraint is
<integer>
. The double colon, ::
, specifies the type constraint of
a slot, just as it specifies the type constraint of a module variable or
of a method’s parameter.
Line 3 is the end of the class definition. The text after the word end
and before the semicolon is an optional part of the definition; it
documents which definition is ending. Any text appearing after the end
must match the definition ending, such as end class <time-of-day>
, or
end class
. You do not need to put any text after the end
— however,
such text is useful for long or complex definitions, where it can be
difficult to see which language construct is ending.
The type constraint of a slot¶
The type constraint of the total-seconds
slot is <integer>
. This
slot can hold instances of <integer>
, and cannot hold any other kind
of object.
The type constraint of a slot is optional. Specifying a slot with no
explicit type constraint is equivalent to specifying <object>
as the
type constraint. A slot whose type constraint is <object>
can hold any
object. The ability to have slots with the type constraint <object>
provides flexibility that can be valuable; for more information, see
Performance and Flexibility.
Use of make
to create an instance¶
We want to make an instance of <time-of-day>
, but first we need a
place to store it. We define a module variable called
*my-time-of-day*
, and initialize it to contain a new instance of
<time-of-day>
:
? define variable *my-time-of-day* = make(<time-of-day>);
The make
function creates an instance of <time-of-day>
. The
argument to make
is the class to create. The make
function returns
the new instance.
The instance stored in *my-time-of-day*
has a total-seconds
slot
with no value. The next logical step is to store a value in that slot.
Getters and setters of slot values¶
We can store a value in the total-seconds
slot of the <time-of-day>
instance by using the assignment operator, :=
, as follows:
? *my-time-of-day*.total-seconds := 180;
=> 180
We can examine the value of the slot in the instance:
? *my-time-of-day*.total-seconds;
=> 180
Although these expressions may look like they are accessing the slots directly, they are not. They are abbreviations for function calls to a getter and a setter. A getter is a method that retrieves the current value of a slot in an object. A setter is a method that stores a value in a slot. Each slot in a class automatically has a getter and a setter defined for it. You can see the function-call syntax, and other information about getters and setters, in Slots.
Initialization of slots when instances are made¶
So far, we have made an instance and set the value of its slot. We might
like to combine those two steps and to set the slot’s value while making
the instance — in other words, to initialize the slot when we make the
instance. One way to do that is to provide a keyword argument to
make
. (Dylan offers several techniques for initializing slots; see
Slots.)
Keyword arguments in function calls¶
We would like to be able to call make
as follows:
? make(<time-of-day>, total-seconds: 120);
We will be able to make this call after we have done a bit of homework,
as we shall show in Init keywords: Keywords that initialize slots.
In the preceding call to make
, we provided a keyword argument,
consisting of a keyword, total-seconds:
, followed by a value, 120
.
The <time-of-day>
instance returned by make
has its
total-seconds
slot set to 120
.
A keyword argument consists of a keyword followed by the keyword’s
value. A keyword is a name followed by a colon, such as
total-seconds:
. The colon after a keyword is not a convention; it is
a required part of the keyword. There must be no space between the name
and the colon.
You can define functions to accept keyword arguments. When a function accepts keyword arguments, you can provide them in any order. Keyword arguments can be useful for functions that take many arguments — when you call the function, you do not need to remember the order of the arguments. Keyword arguments are optional arguments, so they are useful for parameters that have a default value that you may want to override at times. For more information about keyword arguments, see Parameter lists.
How does make
know that the value of the total-seconds:
keyword
should be used to initialize the total-seconds
slot? The keyword and
the slot happen to have the same name, but that is not how it knows.
Before you can use the total-seconds:
keyword argument to make
, you
must associate that keyword with the total-seconds
slot in the class
definition.
Init keywords: Keywords that initialize slots¶
The total-seconds:
keyword is an init keyword — a keyword that we
can give to make
to provide an initial value for a slot. To make it
possible to give an init keyword to make
, we need to use the
init-keyword:
slot option when we define the class. A slot option
lets us specify a characteristic of a slot. Slot options appear after
the optional type specifier of a slot.
Here, we redefine the <time-of-day>
class to use the init-keyword:
slot option:
1 // A specific time of day from 00:00 (midnight) to below 24:00 (tomorrow)
2 define class <time-of-day> (<object>)
3 slot total-seconds :: <integer>, init-keyword: total-seconds:;
4 end class <time-of-day>;
The preceding definition redefines the class <time-of-day>
. That
is, this new definition of <time-of-day>
replaces the old definition
of <time-of-day>
.
In line 3, the init-keyword:
slot option defines total-seconds:
as a
keyword parameter that we can give to make
when we make an instance of
this class. Now that we have defined total-seconds:
as an init
keyword, we can provide the keyword argument as follows:
? *my-time-of-day* := make(<time-of-day>, total-seconds: 120);
=> {instance of <time-of-day>}
The preceding expression creates a new instance of <time-of-day>
, and
stores that instance in the variable *my-time-of-day*
. The value of
the total-seconds
slot of this instance is initialized to 120
. The
assignment operator returns the new value stored; in the preceding call,
the new value is the newly created instance of <time-of-day>
, which
the listener displays as {instance of <time-of-day>}
.
We can use the getter to verify that the slot has an initial value:
? *my-time-of-day*.total-seconds;
=> 120
If you call make
and provide a keyword that has not been declared as a
valid keyword for the class, you get an error; for example,
? make(<time-of-day>, seconds: 120);
=> ERROR: seconds: is not a valid keyword argument to make for {class <time-of-day>}
Methods for handling time¶
We decided to represent the time of day with a single slot named
total-seconds
. An alternate choice would be to give the class three
slots, named hours
, minutes
, and seconds
. People naturally
think of time in terms of hours, minutes, and seconds. We chose to store
the total seconds instead, because we envisioned needing to operate on
times, such as adding a time of day to a time offset. For example, if it
is 9:00 now, and a meeting is to be held 2.5 hours from now, then the
meeting will be held at 11:30. It is easier to operate on a single
value, rather than on three values of hours, minutes, and seconds. On
the other hand, it is convenient to see times expressed as hours,
minutes, and seconds. We can represent the instances with a single slot,
and can provide methods that let users create and see <time-of-day>
instances as being hours, minutes, and seconds.
Method for encode-total-seconds
¶
We can provide a method that converts from hours, minutes, and seconds to total seconds:
1 define method encode-total-seconds
2 (hours :: <integer>, minutes :: <integer>, seconds :: <integer>)
3 => (total-seconds :: <integer>)
4 ((hours * 60) + minutes) * 60 + seconds;
5 end method encode-total-seconds;
Line 2 contains the parameter list of the method encode-total-seconds
.
The method has three required parameters, named hours
, minutes
,
and seconds
, each of type <integer>
. This method is invoked when
encode-total-seconds
is called with three integer arguments.
Line 3 contains the value declaration, which starts with the
characters =>
. It is a list declaring the values returned by the
method. Each element of the list contains a descriptive name of the
return value and the type of the value (if the type is omitted, it is
<object>
). In this case, there is one value returned, named
total-seconds
, which is of the type <integer>
. The name of a
return value is used purely for documentation purposes. Although methods
are not required to have value declarations, there are advantages to
supplying those declarations. When you provide a value declaration for a
method, the compiler signals an error if the method tries to return a
value of the wrong type, can check receivers of the results of the
method for correct type, and can usually produce more efficient code.
These advantages are significant, so we use value declarations
throughout the rest of this book. For more information about value
declarations, see Value declarations.
Line 4 is the only expression in the body. It uses arithmetic functions to convert the hours, minutes, and seconds into total seconds. All methods return the value of the expression executed last in the body. This method returns the result of the arithmetic expression in line 4.
In line 5, we could have simply used end;
. We provided end method
decode-total-seconds;
for documentation purposes. Throughout the rest
of this book, we provide the extra words after the end
of a
definition.
We can call encode-total-seconds
with arguments representing 8 hours,
30 minutes, and 59 seconds:
? encode-total-seconds(8, 30, 59);
=> 30659
We find it convenient to call encode-total-seconds
to initialize the
total-seconds
slot when we create an instance of <time-of-day>
, or when
we store a new value in that slot. Here, for example, we create a new instance:
? define variable *your-time-of-day*
= make(<time-of-day>, total-seconds: encode-total-seconds(8, 30, 59));
We examine the value of the total-seconds
slot:
? *your-time-of-day*.total-seconds;
=> 30659
The result reminds us that it would be useful to convert in the other direction as well — from total seconds to hours, minutes, and seconds.
Method for decode-total-seconds
¶
We define decode-total-seconds
to convert in the other direction —
from total seconds to hours, minutes, and seconds:
1define method decode-total-seconds
2 (total-seconds :: <integer>)
3 => (hours :: <integer>, minutes :: <integer>, seconds :: <integer>)
4 let (total-minutes, seconds) = truncate/(total-seconds, 60);
5 let (hours, minutes) = truncate/(total-minutes, 60);
6 values(hours, minutes, seconds);
7end method decode-total-seconds;
We can use decode-total-seconds
to see the value of the
total-seconds
slot:
? decode-total-seconds(*your-time-of-day*.total-seconds);
=> 8
=> 30
=> 59
The value declaration on line 3 specifies that decode-total-seconds
returns three separate values: the hours, minutes, and seconds. This
method illustrates how to return multiple values, and how to use let
to initialize multiple local variables. We describe these techniques in
Sections Multiple return values and Use of let to declare local variables.
Multiple return values¶
The method for decode-total-seconds
returns three values: the hours,
the minutes, and the seconds. To return the three values, the method
uses the values
function as the expression executed last in the body.
The values
function simply returns all its arguments as separate
values. The ability to return multiple values allows a natural symmetry
between encode-total-seconds
and decode-total-seconds
, as shown in
symmetry-of-encode-decode.
Method |
Parameter(s) |
Return value(s) |
---|---|---|
|
|
|
|
|
|
Lines 4 and 5 of the decode-total-seconds
method contain calls to
truncate/
. The truncate/
function is a built-in Dylan function. It
takes two arguments, divides the first by the second, and returns two
values: the result of the truncating division, and the remainder.
Use of let to declare local variables¶
When a function returns multiple values, you can use let
to store each
returned value in a local variable, as shown in lines 2 and 3 of the
decode-total-seconds
method in Method for decode-total-seconds.
On line 2, we use let
to declare two local variables, named
total-minutes
and seconds
, and to initialize their values to the
two values returned by the truncate/
function. Similarly, on line 3,
we use let
to declare the local variables hours
and minutes
.
The local variables declared by let
can be used within the method
until the method’s end
. Although there is no begin
to define
explicitly the beginning of a body for local variables, define method
begins a body, and its end
finishes that body. Local variables are
scoped within the smallest body that surrounds them, so you can use
begin
and end
within a method to define a smaller body for local
variables, although doing so is usually not necessary.
Second method for decode-total-seconds
¶
The decode-total-seconds
method is called as follows:
? decode-total-seconds(*your-time-of-day*.total-seconds);
If we envision calling decode-total-seconds
frequently to see the
hours, minutes, and seconds stored in a <time-of-day>
instance, we can
make it possible to decode <time-of-day>
instances, as well as
integers. For example, we can make it possible to make this call:
? decode-total-seconds(*your-time-of-day*);
We can implement this behavior easily, by defining another method for
decode-total-seconds
, which takes a <time-of-day>
instance as its
argument:
define method decode-total-seconds
(time :: <time-of-day>)
=> (hours :: <integer>, minutes :: <integer>, seconds :: <integer>)
decode-total-seconds(time.total-seconds);
end method decode-total-seconds;)
The decode-total-seconds generic function and its methods shows the two methods for the
decode-total-seconds
generic function.
The decode-total-seconds
generic function and its methods.
// Method on <integer>
define method decode-total-seconds
(total-seconds :: <integer>)
=> (hours :: <integer>, minutes :: <integer>, seconds :: <integer>)
let (total-minutes, seconds) = truncate/(total-minutes, 60);
values(hours, minutes, seconds);
end method decode-total-seconds;
// Method on <time-of-day>
define method decode-total-seconds
(time :: <time-of-day>)
=> (hours :: <integer>, minutes :: <integer>, seconds :: <integer>)
decode-total-seconds(time.total-seconds);
end method decode-total-seconds;
Looking at The decode-total-seconds generic function and its methods, we analyze what happens in this call:
? decode-total-seconds(*your-time-of-day*);
The argument is an instance of
<time-of-day>
, so the method on<time-of-day>
is called.The body of the method on
<time-of-day>
callsdecode-total-seconds
on an instance of<integer>
, the value of thetotal-seconds
slot of the<time-of-day>
instance. In this call, the argument is an integer, so the method on<integer>
is called.The method on
<integer>
returns three values to its caller — the method on<time-of-day>
. The method on<time-of-day>
returns those three values.
The purpose of the method on <time-of-day>
is simply to allow a
different kind of argument to be used. The method extracts the integer
from the <time-of-day>
instance, and calls decode-total-seconds
with
that integer.
Method for say-time-of-day
¶
We can provide a way to ask an instance of <time-of-day>
to describe
the time in a conventional format, such as 8:30. For the application
that we are planning, there is no need to view the seconds. We want the
method to print the description in a window on the screen. We define a
method named say-time-of-day
:
1 define method say-time-of-day (time :: <time-of-day>) => ()
2 let (hours, minutes) = decode-total-seconds(time);
3 format-out
4 ("%d:%s%d", hours, if (minutes < 10) "0" else "" end, minutes);
5 end method say-time-of-day;
On line 1, we provide an empty value declaration, which means that this method returns no values.
On line 2, we use let
to initialize two local variables to the first
and second values returned by decode-total-seconds
. Remember that
decode-total-seconds
returns three values (the third value is the
seconds). For the application that we are planning, the
say-time-of-day
method does not need to show the seconds, so we do not
use the third value. It is not necessary to receive the third value of
decode-total-seconds
; here we do not provide a local variable to
receive the third value, so that value is simply ignored.
On line 4, we use if
to print a leading 0 for the minutes when there
are fewer than 10 minutes, such as 2:05
.
We can call say-time-of-day
:
? say-time-of-day(*your-time-of-day*);
=> 8:30
? say-time-of-day(*my-time-of-day*);
=> 0:02
The listener displays the output (printed by format-out
), but
displays no values, because say-time-of-day
does not return any
values.
Summary¶
In this chapter, we covered the following:
We defined a class (with
define class
).We created an instance (with
make
).We read the value of a slot by calling a getter.
We set the value of a slot by using
:=
, the assignment operator.We defined a method that returns multiple values (with
values
), and showed how to initialize multiple local variables (withlet
).We showed the syntax of some commonly used elements of Dylan; see Syntax of Dylan elements..
Dylan element |
Syntax example |
---|---|
calling a getter |
|
calling a setter |
|
keyword |
|
single-line comment |
|
multiline comment |
|
value declaration |
|