403 lines
16 KiB
Racket
403 lines
16 KiB
Racket
#lang scribble/doc
|
|
@require[scribble/manual]
|
|
@require[scribble/eval]
|
|
@require[scheme/class]
|
|
@require["guide-utils.ss"]
|
|
|
|
@; FIXME: at some point, discuss classes vs. units vs. modules
|
|
|
|
@title[#:tag "classes"]{Classes and Objects}
|
|
|
|
A @scheme[class] expression denotes a first-class value,
|
|
just like a @scheme[lambda] expression:
|
|
|
|
@specform[(class superclass-expr decl-or-expr ...)]
|
|
|
|
The @scheme[_superclass-expr] determines the superclass for the new
|
|
class. Each @scheme[_decl-or-expr] is either a declaration related to
|
|
methods, fields, and intialization arguments, or it is an expression
|
|
that is evaluated each time that the class is instantiated. In other
|
|
words, instead of a method-like constructor, a class has
|
|
initialization expressions interleaved with field and method
|
|
declarations.
|
|
|
|
By convention, class names end with @schemeidfont{%}. The built-in root class is
|
|
@scheme[object%]. The following expression creates a class with
|
|
public methods @scheme[get-size], @scheme[grow], and @scheme[eat]:
|
|
|
|
@schemeblock[
|
|
(class object%
|
|
(init size) (code:comment #,(t "initialization argument"))
|
|
|
|
(define current-size size) (code:comment #,(t "field"))
|
|
|
|
(super-new) (code:comment #,(t "superclass initialization"))
|
|
|
|
(define/public (get-size)
|
|
current-size)
|
|
|
|
(define/public (grow amt)
|
|
(set! current-size (+ amt current-size)))
|
|
|
|
(define/public (eat other-fish)
|
|
(grow (send other-fish get-size))))
|
|
]
|
|
|
|
@interaction-eval[
|
|
(define fish%
|
|
(class object%
|
|
(init size)
|
|
(define current-size size)
|
|
(super-new)
|
|
(define/public (get-size)
|
|
current-size)
|
|
(define/public (grow amt)
|
|
(set! current-size (+ amt current-size)))
|
|
(define/public (eat other-fish)
|
|
(grow (send other-fish get-size)))))]
|
|
|
|
The @scheme[size] initialization argument must be supplied via a named
|
|
argument when instantiating the class through the @scheme[new] form:
|
|
|
|
@schemeblock[
|
|
(new (class object% (init size) ...) [size 10])
|
|
]
|
|
|
|
Of course, we can also name the class and its instance:
|
|
|
|
@schemeblock[
|
|
(define fish% (class object% (init size) ...))
|
|
(define charlie (new fish% [size 10]))
|
|
]
|
|
|
|
@interaction-eval[(define charlie (new fish% [size 10]))]
|
|
|
|
In the definition of @scheme[fish%], @scheme[current-size] is a
|
|
private field that starts out with the value of the @scheme[size]
|
|
initialization argument. Initialization arguments like @scheme[size]
|
|
are available only during class instantiation, so they cannot be
|
|
referenced directly from a method. The @scheme[current-size] field, in
|
|
contrast, is available to methods.
|
|
|
|
The @scheme[(super-new)] expression in @scheme[fish%] invokes the
|
|
initialization of the superclass. In this case, the superclass is
|
|
@scheme[object%], which takes no initialization arguments and performs
|
|
no work; @scheme[super-new] must be used, anyway, because a class must
|
|
always invoke its superclass's initialization.
|
|
|
|
Initialization arguments, field declarations, and expressions such as
|
|
@scheme[(super-new)] can appear in any order within a @scheme[class],
|
|
and they can be interleaved with method declarations. The relative
|
|
order of expressions in the class determines the order of evaluation
|
|
during instantiation. For example, if a field's initial value requires
|
|
calling a method that works only after superclass initialization, then
|
|
the field declaration must be placed after the @scheme[super-new]
|
|
call. Ordering field and initialization declarations in this way helps
|
|
avoid imperative assignment. The relative order of method declarations
|
|
makes no difference for evaluation, because methods are fully defined
|
|
before a class is instantiated.
|
|
|
|
@section[#:tag "methods"]{Methods}
|
|
|
|
Each of the three @scheme[define/public] declarations in
|
|
@scheme[fish%] introduces a new method. The declaration uses the same
|
|
syntax as a Scheme function, but a method is not accessible as an
|
|
independent function. A call to the @scheme[grow] method of a
|
|
@scheme[fish%] object requires the @scheme[send] form:
|
|
|
|
@interaction[
|
|
(send charlie grow 6)
|
|
(send charlie get-size)
|
|
]
|
|
|
|
Within @scheme[fish%], self methods can be called like functions,
|
|
because the method names are in scope. For example, the @scheme[eat]
|
|
method within @scheme[fish%] directly invokes the @scheme[grow]
|
|
method. Within a class, attempting to use a method name in any way
|
|
other than a method call results in a syntax error.
|
|
|
|
In some cases, a class must call methods that are supplied by the superclass
|
|
but not overridden. In that case, the class can use @scheme[send]
|
|
with @scheme[this] to access the method:
|
|
|
|
@def+int[
|
|
(define hungry-fish% (class fish% (super-new)
|
|
(define/public (eat-more fish1 fish2)
|
|
(send this eat fish1)
|
|
(send this eat fish2))))
|
|
]
|
|
|
|
Alternately, the class can declare the existence of a method using @scheme[inherit],
|
|
which brings the method name into scope for a direct call:
|
|
|
|
@def+int[
|
|
(define hungry-fish% (class fish% (super-new)
|
|
(inherit eat)
|
|
(define/public (eat-more fish1 fish2)
|
|
(eat fish1) (eat fish2))))
|
|
]
|
|
|
|
With the @scheme[inherit] declaration, if @scheme[fish%] had not
|
|
provided an @scheme[eat] method, an error would be signaled in the
|
|
evaluation of the @scheme[class] form for @scheme[hungry-fish%]. In
|
|
contrast, with @scheme[(send this ...)], an error would not be
|
|
signaled until the @scheme[eat-more] method is called and the
|
|
@scheme[send] form is evaluated. For this reason, @scheme[inherit] is
|
|
preferred.
|
|
|
|
Another drawback of @scheme[send] is that it is less efficient than
|
|
@scheme[inherit]. Invocation of a method via @scheme[send] involves
|
|
finding a method in the target object's class at run time, making
|
|
@scheme[send] comparable to an interface-based method call in Java. In
|
|
contrast, @scheme[inherit]-based method invocations use an offset
|
|
within the class's method table that is computed when the class is
|
|
created.
|
|
|
|
To achieve performance similar to @scheme[inherit]-based method calls when
|
|
invoking a method from outside the method's class, the programmer must use the
|
|
@scheme[generic] form, which produces a class- and method-specific
|
|
@defterm{generic method} to be invoked with @scheme[send-generic]:
|
|
|
|
@def+int[
|
|
(define get-fish-size (generic fish% get-size))
|
|
(send-generic charlie get-fish-size)
|
|
(send-generic (new hungry-fish% [size 32]) get-fish-size)
|
|
(send-generic (new object%) get-fish-size)
|
|
]
|
|
|
|
Roughly speaking, the form translates the class and the external
|
|
method name to a location in the class's method table. As illustrated
|
|
by the last example, sending through a generic method checks that its
|
|
argument is an instance of the generic's class.
|
|
|
|
Whether a method is called directly within a @scheme[class],
|
|
through a generic method,
|
|
or through @scheme[send], method overriding works in the usual way:
|
|
|
|
@defs+int[
|
|
[
|
|
(define picky-fish% (class fish% (super-new)
|
|
(define/override (grow amt)
|
|
;; Doesn't eat all of its food
|
|
(super grow (* 3/4 amt)))))
|
|
(define daisy (new picky-fish% [size 20]))
|
|
]
|
|
(send daisy eat charlie)
|
|
(send daisy get-size)
|
|
]
|
|
|
|
The @scheme[grow] method in @scheme[picky-fish%] is declared with
|
|
@scheme[define/override] instead of @scheme[define/public], because
|
|
@scheme[grow] is meant as an overriding declaration. If @scheme[grow]
|
|
had been declared with @scheme[define/public], an error would have
|
|
been signaled when evaluating the @scheme[class] expression, because
|
|
@scheme[fish%] already supplies @scheme[grow].
|
|
|
|
Using @scheme[define/override] also allows the invocation of the
|
|
overridden method via a @scheme[super] call. For example, the
|
|
@scheme[grow] implementation in @scheme[picky-fish%] uses
|
|
@scheme[super] to delegate to the superclass implementation.
|
|
|
|
@section[#:tag "initargs"]{Initialization Arguments}
|
|
|
|
Since @scheme[picky-fish%] declares no initialization arguments, any
|
|
initialization values supplied in @scheme[(new picky-fish% ...)] are
|
|
propagated to the superclass initialization, i.e., to @scheme[fish%].
|
|
A subclass can supply additional initialization arguments for its
|
|
superclass in a @scheme[super-new] call, and such initialization
|
|
arguments take precedence over arguments supplied to @scheme[new]. For
|
|
example, the following @scheme[size-10-fish%] class always generates
|
|
fish of size 10:
|
|
|
|
@def+int[
|
|
(define size-10-fish% (class fish% (super-new [size 10])))
|
|
(send (new size-10-fish%) get-size)
|
|
]
|
|
|
|
In the case of @scheme[size-10-fish%], supplying a @scheme[size]
|
|
initialization argument with @scheme[new] would result in an
|
|
initialization error; because the @scheme[size] in @scheme[super-new]
|
|
takes precedence, a @scheme[size] supplied to @scheme[new] would have
|
|
no target declaration.
|
|
|
|
An initialization argument is optional if the @scheme[class] form
|
|
declares a default value. For example, the following @scheme[default-10-fish%]
|
|
class accepts a @scheme[size] initialization argument, but its value defaults to
|
|
10 if no value is supplied on instantiation:
|
|
|
|
@def+int[
|
|
(define default-10-fish% (class fish%
|
|
(init [size 10])
|
|
(super-new [size size])))
|
|
(new default-10-fish%)
|
|
(new default-10-fish% [size 20])
|
|
]
|
|
|
|
In this example, the @scheme[super-new] call propagates its own
|
|
@scheme[size] value as the @scheme[size] initialization argument to
|
|
the superclass.
|
|
|
|
@section[#:tag "intnames"]{Internal and External Names}
|
|
|
|
The two uses of @scheme[size] in @scheme[default-10-fish%] expose the
|
|
double life of class-member identifiers. When @scheme[size] is the
|
|
first identifier of a bracketed pair in @scheme[new] or
|
|
@scheme[super-new], @scheme[size] is an @defterm{external name} that
|
|
is symbolically matched to an initialization argument in a class. When
|
|
@scheme[size] appears as an expression within
|
|
@scheme[default-10-fish%], @scheme[size] is an @defterm{internal name}
|
|
that is lexically scoped. Similarly, a call to an inherited
|
|
@scheme[eat] method uses @scheme[eat] as an internal name, whereas a
|
|
@scheme[send] of @scheme[eat] uses @scheme[eat] as an external name.
|
|
|
|
The full syntax of the @scheme[class] form allows a programmer to
|
|
specify distinct internal and external names for a class member. Since
|
|
internal names are local, they can be renamed to avoid shadowing or
|
|
conflicts. Such renaming is not frequently necessary, but workarounds
|
|
in the absence of renaming can be especially cumbersome.
|
|
|
|
@section{Interfaces}
|
|
|
|
Interfaces are useful for checking that an object or a class
|
|
implements a set of methods with a particular (implied) behavior.
|
|
This use of interfaces is helpful even without a static type system
|
|
(which is the main reason that Java has interfaces).
|
|
|
|
An interface in PLT Scheme is created using the @scheme[interface]
|
|
form, which merely declares the method names required to implement the
|
|
interface. An interface can extend other interfaces, which means that
|
|
implementations of the interface automatically implement the extended
|
|
interfaces.
|
|
|
|
@specform[(interface (superinterface-expr ...) id ...)]
|
|
|
|
To declare that a class implements an interface, the
|
|
@scheme[class*] form must be used instead of @scheme[class]:
|
|
|
|
@specform[(class* superclass-expr (interface-expr ...) decl-or-expr ...)]
|
|
|
|
For example, instead of forcing all fish classes to be derived from
|
|
@scheme[fish%], we can define @scheme[fish-interface] and change the
|
|
@scheme[fish%] class to declare that it implements
|
|
@scheme[fish-interface]:
|
|
|
|
@schemeblock[
|
|
(define fish-interface (interface () get-size grow eat))
|
|
(define fish% (class* object% (fish-interface) ...))
|
|
]
|
|
|
|
If the definition of @scheme[fish%] does not include
|
|
@scheme[get-size], @scheme[grow], and @scheme[eat] methods, then an
|
|
error is signaled in the evaluation of the @scheme[class*] form,
|
|
because implementing the @scheme[fish-interface] interface requires
|
|
those methods.
|
|
|
|
The @scheme[is-a?] predicate accepts either a class or interface as
|
|
its first argument and an object as its second argument. When given a
|
|
class, @scheme[is-a?] checks whether the object is an instance of that
|
|
class or a derived class. When given an interface, @scheme[is-a?]
|
|
checks whether the object's class implements the interface. In
|
|
addition, the @scheme[implementation?] predicate checks whether a
|
|
given class implements a given interface.
|
|
|
|
@section[#:tag "inner"]{Final, Augment, and Inner}
|
|
|
|
As in Java, a method in a @scheme[class] form can be specified as
|
|
@defterm{final}, which means that a subclass cannot override the
|
|
method. A final method is declared using @scheme[public-final] or
|
|
@scheme[override-final], depending on whether the declaration is for a
|
|
new method or an overriding implementation.
|
|
|
|
Between the extremes of allowing arbitrary overriding and disallowing
|
|
overriding entirely, the class system also supports Beta-style
|
|
@defterm{augmentable} methods~\cite{beta}. A method
|
|
declared with @scheme[pubment] is like @scheme[public], but the method
|
|
cannot be overridden in subclasses; it can be augmented only. A
|
|
@scheme[pubment] method must explicitly invoke an augmentation (if any)
|
|
using @scheme[inner]; a subclass augments the method using
|
|
@scheme[augment], instead of @scheme[override].
|
|
|
|
In general, a method can switch between augment and override modes in
|
|
a class derivation. The @scheme[augride] method specification
|
|
indicates an augmentation to a method where the augmentation is itself
|
|
overrideable in subclasses (though the superclass's implementation
|
|
cannot be overridden). Similarly, @scheme[overment] overrides a method
|
|
and makes the overriding implementation augmentable. Our earlier
|
|
work~\cite{Super+Inner} motivates and explains these extensions and
|
|
their interleaving.
|
|
|
|
@section[#:tag "extnames"]{Controlling the Scope of External Names}
|
|
|
|
As noted in @secref["intnames"], class members have both
|
|
internal and external names. A member definition binds an internal
|
|
name locally, and this binding can be locally renamed. External
|
|
names, in contrast, have global scope by default, and a member
|
|
definition does not bind an external name. Instead, a member
|
|
definition refers to an existing binding for an external name, where
|
|
the member name is bound to a @defterm{member key}; a class ultimately
|
|
maps member keys to methods, fields, and initialization arguments.
|
|
|
|
Recall the @scheme[hungry-fish%] @scheme[class] expression:
|
|
|
|
@schemeblock[
|
|
(define hungry-fish% (class fish% ...
|
|
(inherit eat)
|
|
(define/public (eat-more fish1 fish2)
|
|
(eat fish1) (eat fish2))))
|
|
]
|
|
|
|
During its evaluation, the @scheme[hungry-fish%] and @scheme[fish%]
|
|
classes refer to the same global binding of @scheme[eat]. At run
|
|
time, calls to @scheme[eat] in @scheme[hungry-fish%] are matched with
|
|
the @scheme[eat] method in @scheme[fish%] through the shared method
|
|
key that is bound to @scheme[eat].
|
|
|
|
The default binding for an external name is global, but a
|
|
programmer can introduce an external-name binding with the
|
|
@scheme[define-member-name] form.
|
|
|
|
@specform[(define-member-name id member-key-expr)]
|
|
|
|
In particular, by using @scheme[(generate-member-key)] as the
|
|
@scheme[member-key-expr], an external name can be localized for a
|
|
particular scope, because the generated member key is inaccessible
|
|
outside the scope. In other words, @scheme[define-member-name] gives
|
|
an external name a kind of package-private scope, but generalized from
|
|
packages to arbitrary binding scopes in Scheme.
|
|
|
|
For example, the following @scheme[fish%] and @scheme[pond%] classes cooperate
|
|
via a @scheme[get-depth] method that is only accessible to the
|
|
cooperating classes:
|
|
|
|
@schemeblock[
|
|
(define-values (fish% pond%) (code:comment #,(t "two mutually recursive classes"))
|
|
(let () ; create a local definition scope
|
|
(define-member-name get-depth (generate-member-key))
|
|
(define fish%
|
|
(class ... (define my-depth ...)
|
|
(define my-pond ...)
|
|
(define/public (dive amt)
|
|
(set! my-depth
|
|
(min (+ my-depth amt)
|
|
(send my-pond get-depth))))))
|
|
(define pond%
|
|
(class ... (define current-depth ...)
|
|
(define/public (get-depth) current-depth)))
|
|
(values fish% pond%)))
|
|
]
|
|
|
|
External names are in a namespace that separates them from other Scheme
|
|
names. This separate namespace is implicitly used for the method name in
|
|
@scheme[send], for initialization-argument names in @scheme[new], or for
|
|
the external name in a member definition. The special form
|
|
@scheme[member-name-key] provides access to the binding of an external name
|
|
in an arbitrary expression position: @scheme[(member-name-key id)]
|
|
produces the member-key binding of @scheme[id] in the current scope.
|
|
|
|
A member-key value is primarily used with a
|
|
@scheme[define-member-name] form. Normally, then,
|
|
@scheme[(member-name-key id)] captures the method key of @scheme[id]
|
|
so that it can be communicated to a use of @scheme[define-member-name]
|
|
in a different scope. This capability turns out to be useful for
|
|
generalizing mixins (see mixins...).
|