racket/collects/scribblings/guide/class.scrbl

806 lines
31 KiB
Racket

#lang scribble/doc
@(require scribble/manual
scribble/eval
scheme/class
"guide-utils.ss"
(for-label scheme/class
scheme/trait))
@(define class-eval
(let ([e (make-base-eval)])
(e '(require scheme/class))
e))
@; FIXME: at some point, discuss classes vs. units vs. modules
@title[#:tag "classes"]{Classes and Objects}
@margin-note{This chapter is based on a paper @cite["Flatt06"].}
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 initialization 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
#:eval class-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
#:eval class-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[
#:eval class-eval
(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[
#:eval class-eval
(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[
#:eval class-eval
(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[
#:eval class-eval
(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[
#:eval class-eval
[
(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[
#:eval class-eval
(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[
#:eval class-eval
(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["Goldberg04"]. 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.
@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, as discussed next.
@; ----------------------------------------------------------------------
@section{Mixins}
Since @scheme[class] is an expression form instead of a top-level
declaration as in Smalltalk and Java, a @scheme[class] form can be
nested inside any lexical scope, including @scheme[lambda]. The result
is a @deftech{mixin}, i.e., a class extension that is parameterized
with respect to its superclass.
For example, we can parameterize the @scheme[picky-fish%] class over
its superclass to define @scheme[picky-mixin]:
@schemeblock[
(define (picky-mixin %)
(class % (super-new)
(define/override (grow amt) (super grow (* 3/4 amt)))))
(define picky-fish% (picky-mixin fish%))
]
Many small differences between Smalltalk-style classes and Scheme
classes contribute to the effective use of mixins. In particular, the
use of @scheme[define/override] makes explicit that
@scheme[picky-mixin] expects a class with a @scheme[grow] method. If
@scheme[picky-mixin] is applied to a class without a @scheme[grow]
method, an error is signaled as soon as @scheme[picky-mixin] is
applied.
Similarly, a use of @scheme[inherit] enforces a ``method existence''
requirement when the mixin is applied:
@schemeblock[
(define (hungry-mixin %)
(class % (super-new)
(inherit eat)
(define/public (eat-more fish1 fish2)
(eat fish1)
(eat fish2))))
]
The advantage of mixins is that we can easily combine them to create
new classes whose implementation sharing does not fit into a
single-inheritance hierarchy---without the ambiguities associated with
multiple inheritance. Equipped with @scheme[picky-mixin] and
@scheme[hungry-mixin], creating a class for a hungry, yet picky fish
is straightforward:
@schemeblock[
(define picky-hungry-fish%
(hungry-mixin (picky-mixin fish%)))
]
The use of keyword initialization arguments is critical for the easy
use of mixins. For example, @scheme[picky-mixin] and
@scheme[hungry-mixin] can augment any class with suitable @scheme[eat]
and @scheme[grow] methods, because they do not specify initialization
arguments and add none in their @scheme[super-new] expressions:
@schemeblock[
(define person%
(class object%
(init name age)
....
(define/public (eat food) ....)
(define/public (grow amt) ....)))
(define child% (hungry-mixin (picky-mixin person%)))
(define oliver (new child% [name "Oliver"][age 6]))
]
Finally, the use of external names for class members (instead of
lexically scoped identifiers) makes mixin use convenient. Applying
@scheme[picky-mixin] to @scheme[person%] works because the names
@scheme[eat] and @scheme[grow] match, without any a priori declaration
that @scheme[eat] and @scheme[grow] should be the same method in
@scheme[fish%] and @scheme[person%]. This feature is a potential
drawback when member names collide accidentally; some accidental
collisions can be corrected by limiting the scope external names, as
discussed in @secref["extnames"].
@subsection{Mixins and Interfaces}
Using @scheme[implementation?], @scheme[picky-mixin] could require
that its base class implements @scheme[grower-interface], which could
be implemented by both @scheme[fish%] and @scheme[person%]:
@schemeblock[
(define grower-interface (interface () grow))
(define (picky-mixin %)
(unless (implementation? % grower-interface)
(error "picky-mixin: not a grower-interface class"))
(class % ....))
]
Another use of interfaces with a mixin is to tag classes generated by
the mixin, so that instances of the mixin can be recognized. In other
words, @scheme[is-a?] cannot work on a mixin represented as a
function, but it can recognize an interface (somewhat like a
@defterm{specialization interface}) that is consistently implemented
by the mixin. For example, classes generated by @scheme[picky-mixin]
could be tagged with @scheme[picky-interface], enabling the
@scheme[is-picky?] predicate:
@schemeblock[
(define picky-interface (interface ()))
(define (picky-mixin %)
(unless (implementation? % grower-interface)
(error "picky-mixin: not a grower-interface class"))
(class* % (picky-interface) ....))
(define (is-picky? o)
(is-a? o picky-interface))
]
@subsection{The @scheme[mixin] Form}
To codify the @scheme[lambda]-plus-@scheme[class] pattern for
implementing mixins, including the use of interfaces for the domain
and range of the mixin, the class system provides a @scheme[mixin]
macro:
@specform[
(mixin (interface-expr ...) (interface-expr ...)
decl-or-expr ...)
]
The first set of @scheme[interface-expr]s determines the domain of the
mixin, and the second set determines the range. That is, the expansion
is a function that tests whether a given base class implements the
first sequence of @scheme[interface-expr]s and produces a class that
implements the second sequence of @scheme[interface-expr]s. Other
requirements, such as the presence of @scheme[inherit]ed methods in
the superclass, are then checked for the @scheme[class] expansion of
the @scheme[mixin] form.
Mixins not only override methods and introduce public methods, they
can also augment methods, introduce augment-only methods, add an
overrideable augmentation, and add an augmentable override --- all of
the things that a class can do (see @secref["inner"]).
@subsection[#:tag "parammixins"]{Parameterized Mixins}
As noted in @secref["extnames"], external names can be bound with
@scheme[define-member-name]. This facility allows a mixin to be
generalized with respect to the methods that it defines and uses. For
example, we can parameterize @scheme[hungry-mixin] with respect to the
external member key for @scheme[eat]:
@schemeblock[
(define (make-hungry-mixin eat-method-key)
(define-member-name eat eat-method-key)
(mixin () () (super-new)
(inherit eat)
(define/public (eat-more x y) (eat x) (eat y))))
]
To obtain a particular hungry-mixin, we must apply this function to a
member key that refers to a suitable
@scheme[eat] method, which we can obtain using @scheme[member-name-key]:
@schemeblock[
((make-hungry-mixin (member-name-key eat))
(class object% .... (define/public (eat x) 'yum)))
]
Above, we apply @scheme[hungry-mixin] to an anonymous class that provides
@scheme[eat], but we can also combine it with a class that provides
@scheme[chomp], instead:
@schemeblock[
((make-hungry-mixin (member-name-key chomp))
(class object% .... (define/public (chomp x) 'yum)))
]
@; ----------------------------------------------------------------------
@section{Traits}
A @defterm{trait} is similar to a mixin, in that it encapsulates a set
of methods to be added to a class. A trait is different from a mixin
in that its individual methods can be manipulated with trait operators
such as @scheme[trait-sum] (merge the methods of two traits), @scheme[trait-exclude]
(remove a method from a trait), and @scheme[trait-alias] (add a copy of a
method with a new name; do not redirect any calls to the old name).
The practical difference between mixins and traits is that two traits
can be combined, even if they include a common method and even if
neither method can sensibly override the other. In that case, the
programmer must explicitly resolve the collision, usually by aliasing
methods, excluding methods, and merging a new trait that uses the
aliases.
Suppose our @scheme[fish%] programmer wants to define two class
extensions, @scheme[spots] and @scheme[stripes], each of which
includes a @scheme[get-color] method. The fish's spot color should not
override the stripe color nor vice-versa; instead, a
@scheme[spots+stripes-fish%] should combine the two colors, which is
not possible if @scheme[spots] and @scheme[stripes] are implemented as
plain mixins. If, however, @scheme[spots] and @scheme[stripes] are
implemented as traits, they can be combined. First, we alias
@scheme[get-color] in each trait to a non-conflicting name. Second,
the @scheme[get-color] methods are removed from both and the traits
with only aliases are merged. Finally, the new trait is used to create
a class that introduces its own @scheme[get-color] method based on the
two aliases, producing the desired @scheme[spots+stripes] extension.
@subsection{Traits as Sets of Mixins}
One natural approach to implementing traits in PLT Scheme is as a set
of mixins, with one mixin per trait method. For example, we might
attempt to define the spots and stripes traits as follows, using
association lists to represent sets:
@schemeblock[
(define spots-trait
(list (cons 'get-color
(lambda (%) (class % (super-new)
(define/public (get-color)
'black))))))
(define stripes-trait
(list (cons 'get-color
(lambda (%) (class % (super-new)
(define/public (get-color)
'red))))))
]
A set representation, such as the above, allows @scheme[trait-sum] and
@scheme[trait-exclude] as simple manipulations; unfortunately, it does
not support the @scheme[trait-alias] operator. Although a mixin can be
duplicated in the association list, the mixin has a fixed method name,
e.g., @scheme[get-color], and mixins do not support a method-rename
operation. To support @scheme[trait-alias], we must parameterize the
mixins over the external method name in the same way that @scheme[eat]
was parameterized in @secref["parammixins"].
To support the @scheme[trait-alias] operation, @scheme[spots-trait]
should be represented as:
@schemeblock[
(define spots-trait
(list (cons (member-name-key get-color)
(lambda (get-color-key %)
(define-member-name get-color get-color-key)
(class % (super-new)
(define/public (get-color) 'black))))))
]
When the @scheme[get-color] method in @scheme[spots-trait] is aliased
to @scheme[get-trait-color] and the @scheme[get-color] method is
removed, the resulting trait is the same as
@schemeblock[
(list (cons (member-name-key get-trait-color)
(lambda (get-color-key %)
(define-member-name get-color get-color-key)
(class % (super-new)
(define/public (get-color) 'black)))))
]
To apply a trait @scheme[_T] to a class @scheme[_C] and obtain a derived
class, we use @scheme[((trait->mixin _T) _C)]. The @scheme[trait->mixin]
function supplies each mixin of @scheme[_T] with the key for the mixin's
method and a partial extension of @scheme[_C]:
@schemeblock[
(define ((trait->mixin T) C)
(foldr (lambda (m %) ((cdr m) (car m) %)) C T))
]
Thus, when the trait above is combined with other traits and then
applied to a class, the use of @scheme[get-color] becomes a reference
to the external name @scheme[get-trait-color].
@subsection{Inherit and Super in Traits}
This first implementation of traits supports @scheme[trait-alias], and it
supports a trait method that calls itself, but it does not support
trait methods that call each other. In particular, suppose that a spot-fish's
market value depends on the color of its spots:
@schemeblock[
(define spots-trait
(list (cons (member-name-key get-color) ....)
(cons (member-name-key get-price)
(lambda (get-price %) ....
(class % ....
(define/public (get-price)
.... (get-color) ....))))))
]
In this case, the definition of @scheme[spots-trait] fails, because
@scheme[get-color] is not in scope for the @scheme[get-price]
mixin. Indeed, depending on the order of mixin application when the
trait is applied to a class, the @scheme[get-color] method may not be
available when @scheme[get-price] mixin is applied to the class.
Therefore adding an @scheme[(inherit get-color)] declaration to the
@scheme[get-price] mixin does not solve the problem.
One solution is to require the use of @scheme[(send this get-color)] in
methods such as @scheme[get-price]. This change works because
@scheme[send] always delays the method lookup until the method call is
evaluated. The delayed lookup is more expensive than a direct call,
however. Worse, it also delays checking whether a @scheme[get-color] method
even exists.
A second, effective, and efficient solution is to change the encoding
of traits. Specifically, we represent each method as a pair of mixins:
one that introduces the method and one that implements it. When a
trait is applied to a class, all of the method-introducing mixins are
applied first. Then the method-implementing mixins can use
@scheme[inherit] to directly access any introduced method.
@schemeblock[
(define spots-trait
(list (list (local-member-name-key get-color)
(lambda (get-color get-price %) ....
(class % ....
(define/public (get-color) (void))))
(lambda (get-color get-price %) ....
(class % ....
(define/override (get-color) 'black))))
(list (local-member-name-key get-price)
(lambda (get-price get-color %) ....
(class % ....
(define/public (get-price) (void))))
(lambda (get-color get-price %) ....
(class % ....
(inherit get-color)
(define/override (get-price)
.... (get-color) ....))))))
]
With this trait encoding, @scheme[trait-alias] adds a new method with
a new name, but it does not change any references to the old method.
@subsection{The @scheme[trait] Form}
The general-purpose trait pattern is clearly too complex for a
programmer to use directly, but it is easily codified in a
@scheme[trait] macro:
@specform[
(trait trait-clause ...)
]
The @scheme[id]s in the optional @scheme[inherit] clause are available for direct
reference in the method @scheme[expr]s, and they must be supplied
either by other traits or the base class to which
the trait is ultimately applied.
Using this form in conjunction with trait operators such as
@scheme[trait-sum], @scheme[trait-exclude], @scheme[trait-alias], and
@scheme[trait->mixin], we can implement @scheme[spots-trait] and
@scheme[stripes-trait] as desired.
@schemeblock[
(define spots-trait
(trait
(define/public (get-color) 'black)
(define/public (get-price) ... (get-color) ...)))
(define stripes-trait
(trait
(define/public (get-color) 'red)))
(define spots+stripes-trait
(trait-sum
(trait-exclude (trait-alias spots-trait
get-color get-spots-color)
get-color)
(trait-exclude (trait-alias stripes-trait
get-color get-stripes-color)
get-color)
(trait
(inherit get-spots-color get-stripes-color)
(define/public (get-color)
.... (get-spots-color) .... (get-stripes-color) ....))))
]
@; ----------------------------------------------------------------------
@close-eval[class-eval]