class.ss: Classes and Objects
To load: (require (lib "class.ss"))
a collection of fields;
a collection of methods;
initial value expressions for the fields; and
initialization variables that are bound to initialization arguments.
An object is a collection of bindings for fields that are instantiated according to a class description.
The object system allows a program to define a new class (a derived class) in terms of an existing class (the superclass) using inheritance, overriding, and augmenting:
inheritance: An object of a derived class supports methods and instantiates fields declared by the derived class's superclass, as well as methods and fields declared in the derived class expression.
overriding: Some methods declared in a superclass can be replaced in the derived class. References to the overridden method in the superclass use the implementation in the derived class.
augmenting: Some methods declared in a superclass can be merely extended in the derived class. The superclass method specifically delegates to the augmenting method in the derived class.
An interface is a collection of method names to be implemented by a class, combined with a derivation requirement. A class implements an interface when it
declares (or inherits) a public method for each variable in the interface;
is derived from the class required by the interface, if any; and
specifically declares its intention to implement the interface.
A class can implement any number of interfaces. A derived class automatically implements any interface that its superclass implements. Each class also implements an implicitly-defined interface that is associated with the class. The implicitly-defined interface contains all of the class's public method names, and it requires that all other implementations of the interface are derived from the class.
A new interface can extend one or more interfaces with additional method names; each class that implements the extended interface also implements the original interfaces. The derivation requirements of the original interface must be consistent, and the extended interface inherits the most specific derivation requirement from the original interfaces.
Classes, objects, and interfaces are all first-class Scheme values. However, a MzScheme class or interface is not a MzScheme object (i.e., there are no ``meta-classes'' or ``meta-interfaces'').
6.1 Object Example
The following example conveys the object system's basic style.
(define stack<%> (interface () push! pop! none?)) (define stack% (class*object%
(stack<%>) ; Declare public methods that can be overridden: (public push! pop! none?) ; Declare a public method that can be augmented, only: (pubment print-name) (define stacknull
) ; A private field (init-field (name 'stack)) ; A public field ; Method implementations: (define (push! v) (set! stack (cons
v stack))) (define (pop!) (let ([v (car
stack)]) (set! stack (cdr
stack)) v)) (define (none?) (null?
stack)) (define (print-name) (display
name) (inner (void
) print-name) ; Let subclass print more (newline
)) ; Call superclass initializer: (super-new))) (define fancy-stack% (class stack% ; Declare override (override push!) ; Implement override: (define (push! v) (super push! (cons
'fancy v))) ; Add inherited field to local environment (inherit-field name) ; Declare augment (augment print-name) ; Implement augment (define (print-name) (when (equal?
name 'Bob) (display
", Esq.")) (inner (void
) print-name)) (super-new))) (define double-stack% (class stack% (inherit push!) (public double-push!) (define (double-push! v) (push! v) (push! v)) ; Always supply name (super-new (name 'double-stack)))) (define-values (make-safe-stack-class is-safe-stack?) (let ([safe-stack<%> (interface (stack<%>))]) (values
(lambda (super%) (class* super% (safe-stack<%>) (inherit none?) (override pop!) (define (pop!) (if (none?) #f (super pop!))) (super-new))) (lambda (obj) (is-a?
obj safe-stack<%>))))) (define safe-stack% (make-safe-stack-class stack%))
The interface stack<%>
1 defines the ever-popular stack
interface with the methods push!
, pop!
, and none?
.
Since it has no superinterfaces, the only derivation requirement of
stack<%>
is that its classes are derived from the built-in
empty class, object%
. The class stack%
2 is derived from
object%
and implements the stack<%>
interface. Three
additional classes are derived from the basic stack%
implementation:
The class
fancy-stack%
defines a stack that overridespush!
to tag each item as fancy. It also augmentsprint-name
to add an ``Esq.'' suffix if the stack's name is'Bob
.The class
double-stack%
extends the functionalitystack%
with a new method,double-push!
. It also supplies a specificname
tostack%
.The class
safe-stack%
overrides thepop!
method ofstack%
, ensuring that#f
is returned whenever the stack is empty.
In each derived class, the (super-new ...)
form causes the
superclass portion of the object to be initialized, including the
initialization of its fields.
The creation of safe-stack%
illustrates the use of classes as
first-class values. Applying make-safe-stack-class
to
fancy-stack%
or double-stack%
-- indeed, any
class with push
, pop!
, and none?
methods --
creates a ``safe'' version of the class. A stack object can be
recognized as a safe stack by testing it with is-safe-stack?
;
this predicate returns #t
only for instances of a class
created with make-safe-stack-class
(because only those classes
implement the safe-stack<%>
interface).
In each of the example classes, the field name
contains the
name of the class. The name
instance variable is introduced
as a new instance variable in stack%
, and it is declared
there with the init-field
keyword, which means that an
instantiation of the class can specify the initial value, but it
defaults to 'stack
. The double-stack%
class
provides name
when initializing the stack%
part of
the object, so a name cannot be supplied when instantiating
double-stack%
. When the print-name
method of an
object from double-stack%
is invoked, the name printed to
the screen is always ``double-stack''.
While all of fancy-stack%
, double-stack%
, and
safe-stack%
inherit the push!
method of
stack%
, it is declared with inherit
only in
double-stack%
; new declarations in fancy-stack%
and
safe-stack%
do not need to refer to push!
, so the
inheritance does not need to be declared. Similarly, only
safe-stack%
needs to declare (inherit none?)
.
The fancy-stack%
class overrides pop!
to extend
the implementation of pop!
. The new definition of
pop!
must accesses the original pop!
method that
is defined in stack%
through the super
form.
The stack%
class declares its print-name
method
using pubment
, which means that the method is public, but it
can only be augmented in subclasses, and not overridden. The
implementation of print-name
uses inner
to execute
a subclass-supplied augmenting method. If no such augmenting method
is available, the (
expression is evaluated,
instead. The void
)fancy-stack%
classes uses augment
to
declare an augmentation of print-name
, and also uses
inner
to allow further augmenting in later subclasses.
The instantiate
form, the new
form, and the
procedure all create an object from a class. The
make-object
instantiate
form supports initialization arguments by both
position and name, the new
form only supports by name
initialization arguments, and
supports
initialization arguments by position only. The following examples
create objects using the classes above:
make-object
(define stack (make-object
stack%)) (define fred (new stack% (name 'Fred))) (define joe (instantiate stack% () (name 'Joe))) (define double-stack (make-object
double-stack%)) (define safe-stack (new safe-stack% (name 'safe)))
The send
form calls a method on an object, finding the method by
name. The following example uses the objects created above:
(send stack push! fred)
(send stack push! double-stack)
(let loop ()
(if (not
(send stack none?))
(begin
(send (send stack pop!) print-name)
(loop))))
This loop displays 'double-stack
and 'Fred
to the
standard output port.
6.2 Creating Interfaces
The interface
form creates a new interface:
(interface (super-interface-expr ···) identifier ···)
All of the identifier
s must be distinct.
Each super-interface-expr
is evaluated (in order) when the
interface
expression is evaluated. The result of each
super-interface-expr
must be an interface value, otherwise the
exn:fail:object
exception is raised. The interfaces returned by the
super-interface-expr
s are the new interface's superinterfaces,
which are all extended by the new interface. Any class that
implements the new interface also implements all of the
superinterfaces.
The result of an interface
expression is an interface that
includes all of the specified identifier
s, plus all identifiers
from the superinterfaces. Duplicate identifier names among the
superinterfaces are ignored, but if a superinterface contains one of
the identifier
s in the interface
expression, the
exn:fail:object
exception is raised.
If no super-interface-expr
s are provided, then the derivation
requirement of the resulting interface is trivial: any class that
implements the interface must be derived from object%
.
Otherwise, the implementation requirement of the resulting interface
is the most specific requirement from its superinterfaces. If the
superinterfaces specify inconsistent derivation requirements, the
exn:fail:object
exception is raised.
6.3 Creating Classes
The built-in class object%
has no methods fields,
implements only its own interface (
, and is transparent (i.e,. its inspector is class->interface
object%)#f
,
so all immediate instances are
). All other classes
are derived from equal?
object%
.
The class*
form creates a new class:
(class* superclass-expr (interface-expr ···)
class-clause
···)
class-clause is one of
(inspect
inspector-expr)
(init init-declaration ···)
(init-field init-declaration ···)
(field field-declaration ···)
(inherit-field optionally-renamed-id ···)
(init-rest id)
(init-rest)
(public optionally-renamed-id ···)
(pubment optionally-renamed-id ···)
(public-final optionally-renamed-id ···)
(override optionally-renamed-id ···)
(overment optionally-renamed-id ···)
(override-final optionally-renamed-id ···)
(augment optionally-renamed-id ···)
(augride optionally-renamed-id ···)
(augment-final optionally-renamed-id ···)
(private id ···)
(inherit optionally-renamed-id ···)
(inherit/super optionally-renamed-id ···)
(inherit/inner optionally-renamed-id ···)
(rename-super renamed-id ···)
(rename-inner renamed-id ···)
method-definition
definition
expr
(begin class-clause ···)
init-declaration is one of
identifier
(optionally-renamed-id)
(optionally-renamed-id default-value-expr)
field-declaration is
(optionally-renamed-id default-value-expr)
optionally-renamed-id is one of
identifier
renamed-id
renamed-id is
(internal-id external-id)
method-definition is
(define-values (identifier) method-procedure)
method-procedure is
(lambda formals expr ···1)
(case-lambda (formals expr ···1) ···)
(let-values (((identifier) method-procedure) ···) method-procedure)
(letrec-values (((identifier) method-procedure) ···) method-procedure)
(let-values (((identifier) method-procedure) ···1) identifier)
(letrec-values (((identifier) method-procedure) ···1) identifier)
The superclass-expr
expression is evaluated when the
class*
expression is evaluated. The result must be a class
value (possibly object%
), otherwise the
exn:fail:object
exception is raised. The result of the superclass-expr
expression is the new class's superclass.
The interface-expr
expressions are also evaluated when the
class*
expression is evaluated, after superclass-expr
is
evaluated. The result of each interface-expr
must be an
interface value, otherwise the exn:fail:object
exception is raised. The
interfaces returned by the interface-expr
s are all implemented
by the class. For each identifier in each interface, the class (or
one of its ancestors) must declare a public method with the same
name, otherwise the exn:fail:object
exception is raised. The class's
superclass must satisfy the implementation requirement of each
interface, otherwise the exn:fail:object
exception is raised.
An inspect
class-clause
selects an
inspector (see section 4.5 in PLT MzScheme: Language Manual) for the class
extension. The inspector-expr
must evaluate to an inspector
or #f
when the class*
form is evaluated. Just as for
structure types, an inspector controls access to the class's fields,
including private fields, and also affects comparisons
using equal?
. If no inspect
clause is provided,
access to the class is controlled by the parent of the current
inspector (see section 4.5 in PLT MzScheme: Language Manual). A syntax error is reported if
more than one inspect
clause is specified.
The other class-clause
s define initialization arguments, public and
private fields, and public and private methods. For each
identifier
or optionally-renamed-id
in a public
,
override
, augment
, pubment
,
overment
, augride
, public-final
,
override-final
, augment-final
, or private
clause, there must be one method-definition
. All other
definition class-clause
s create private fields. All remaining
expr
s are initialization expressions to be evaluated when the
class is instantiated (see section 6.4).
The result of a class*
expression is a new class, derived from
the specified superclass and implementing the specified
interfaces. Instances of the class are created with the
instantiate
form or make-object
procedure, as
described in section 6.4.
Each class-clause
is (partially) macro-expanded to reveal its
shapes. If a class-clause
is a begin
expression,
its sub-expressions are lifted out of the begin
and treated
as class-clause
s, in the same way that begin
is
flattened for top-level and embedded definitions.
Within a class*
form for instances of the new class,
this
is bound to the object itself;
super-instantiate
,
, and
super-make-object
super-new
are bound to forms to initialize fields in the
superclass (see section 6.4); super
is available
for calling superclass methods (see section 6.3.3.1); and
inner
is available for calling subclass augmentations of
methods (see section 6.3.3.1).
The public
, override
, augment
, pubment
,
overment
, augride
, public-final
,
override-final
, augment-final
, private
,
inherit
, inherit/super
, inherit/inner
,
rename-super
, rename-inner
this
, super
, inner
,
super-instantiate
,
, and
super-make-object
super-new
keywords are all exported by class.ss as
syntactic forms that raise an error when used outside of a class
declaration.
The class
form is like class*
, but omits the
interface-expr
s, for the case that none are needed:
(class superclass-expr class-clause ···)
The public*
, pubment*
,
public-final*
, override*
,
overment*
, override-final*
,
augment*
, augride*
,
augment-final*
, and private*
forms
abbreviate a public
, etc. declaration and a sequence of
definitions:
(public* (name expr) ···) =expands=> (begin (public name ···) (define name expr) ···) etc.
The define/public
, define/pubment
,
define/public-final
, define/override
,
define/overment
, define/override-final
,
define/augment
, define/augride
,
define/augment-final
, and define/private
forms similarly abbreviate a public
, etc. declaration with
a definition:
(define/public name expr) =expands=> (begin (public name) (define name expr)) (define/public (name . formals) expr) =expands=> (begin (public name) (define (name . formals) expr)) etc.
6.3.1 Initialization Variables
A class's initialization variables, declared with init
,
init-field
, and init-rest
, are instantiated
for each object of a class. Initialization variables can be used in
the initial value expressions of fields, default value expressions
for initialization arguments, and in initialization expressions. Only
initialization variables declared with init-field
can be
accessed from methods; accessing any other initialization variable
from a method is a syntax error.
The values bound to initialization variables are
the arguments provided with
instantiate
or passed tomake-object
, if the object is created as a direct instance of the class; or,the arguments passed to the superclass initialization form or procedure, if the object is created as an instance of a derived class.
If an initialization argument is not provided for an initialization
variable that has an associated default-value-expr
, then the
default-value-expr
expression is evaluated to obtain a value
for the variable. A default-value-expr
is only evaluated when
an argument is not provided for its variable. The environment of
default-value-expr
includes all of the initialization
variables, all of the fields, and all of the methods of the class. If
multiple default-value-expr
s are evaluated, they are evaluated
from left to right. Object creation and field initialization are
described in detail in section 6.4.
If an initialization variable has no default-value-expr
, then
the object creation or superclass initialization call must supply an
argument for the variable, otherwise the exn:fail:object
exception is raised.
Initialization arguments can be provided by name or by position. The
external name of an initialization variable can be used with
instantiate
or with the superclass initialization form. Those
forms also accept by-position arguments. The make-object
procedure and the superclass initialization procedure accept only
by-position arguments.
Arguments provided by position are converted into by-name arguments
using the order of init
and init-field
clauses and
the order of variables within each clause. When a instantiate
form provides both by-position and by-name arguments, the converted
arguments are placed before by-name arguments. (The order can be
significant; see also section 6.4.)
Unless a class contains an init-rest
clause, when the number
of by-position arguments exceeds the number of declared
initialization variables, the order of variables in the superclass
(and so on, up the superclass chain) determines the by-name
conversion.
If a class expression contains an init-rest
clause, there
must be only one, and it must be last. If it declares a variable,
then the variable receives extra by-position initialization arguments
as a list (similar to a dotted ``rest argument'' in a procedure). An
init-rest
variable can receive by-position initialization
arguments that are left over from a by-name conversion for a derived
class. When a derived class's superclass initialization provides even
more by-position arguments, they are prefixed onto the by-position
arguments accumulated so far.
If too few or too many by-position initialization arguments are
provided to an object creation or superclass initialization, then the
exn:fail:object
exception is raised. Similarly, if extra by-position arguments are
provided to a class with an init-rest
clause, the
exn:fail:object
exception is raised.
Unused (by-name) arguments are to be propagated to the superclass, as described in section 6.4. Multiple initialization arguments can use the same name if the class derivation contains multiple declarations (in different classes) of initialization variables with the name. See section 6.4 for further details.
See also section 6.3.3.3 for information about internal and external names.
6.3.2 Fields
Each field
, init-field
, and non-method
define-values
clause in a class declares one or more new
fields for the class. Fields declared with field
or
init-field
are public. Public fields can be accessed and
mutated by subclasses using inherit-field
. Public fields are
also accessible outside the class via class-field-accessor
and mutable via class-field-mutator
(see
section 6.5). Fields declared with define-values
are
accessible only within the class.
A field declared with init-field
is both a public field and an
initialization variable. See section 6.3.1 for information
about initialization variables.
An inherit-field
declaration makes a public field defined
by a superclass directly accessible in the class expression. If the
indicated field is not defined in the superclass, the
exn:fail:object
exception is raised when the class expression is evaluated. Every
field in a superclass is present in a derived class, even if it is
not declared with inherit-field
in the derived class. The
inherit-field
clause does not control inheritance, but
merely controls lexical scope within a class expression.
When an object is first created, all of its fields have the undefined value (see section 3.1 in PLT MzScheme: Language Manual). The fields of a class are initialized at the same time that the class's initialization expressions are evaluated; see section 6.4 for more information.
See also section 6.3.3.3 for information about internal and external names.
6.3.3 Methods
6.3.3.1 Method Definitions
Each public
, override
, augment
,
pubment
, overment
, augride
,
public-final
, override-final
,
augment-final
, and private
clause in a
class declares one or more method names. Each method name must have a
corresponding method-definition
. The order of public
,
etc. clauses and their corresponding definitions (among themselves,
and with respect to other clauses in the class) does not matter.
As shown in section 6.3, a method definition is syntactically
restricted to certain procedure forms, as defined by the grammar for
method-procedure
; in the last two forms of
method-procedure
, the body identifier
must be one of the
identifier
s bound by let-values
or
letrec-values
. A method-procedure
expression is not
evaluated directly. Instead, for each method, a class-specific method
procedure is created; it takes an initial object argument, in
addition to the arguments the procedure would accept if the
method-procedure
expression were evaluated directly. The
body of the procedure is transformed to access methods and fields
through the object argument.
A method declared with public
, pubment
, or
public-final
introduces a new method into a class. The
method must not be present already in the superclass, otherwise the
exn:fail:object
exception is raised when the class expression is evaluated. A
method declared with public
can be overridden in a subclass
that uses override
, overment
, or
override-final
. A method declared with pubment
can
be augmented in a subclass that uses augment
,
augride
, or augment-final
. A method declared with
public-final
cannot be overridden or augmented in a
subclass.
A method declared with override
, overment
, or
override-final
overrides a definition already present in the
superclass. If the method is not already present, the
exn:fail:object
exception is raised when the class expression is evaluated. A
method declared with override
can be overridden again in a
subclass that uses override
, overment
, or
override-final
. A method declared with overment
can be augmented in a subclass that uses augment
,
augride
, or augment-final
. A method declared with
override-final
cannot be overridden further or augmented in
a subclass.
A method declared with augment
, augride
, or
augment-final
augments a definition already present in the
superclass. If the method is not already present, the
exn:fail:object
exception is raised when the class expression is evaluated. A
method declared with augment
can be augmented further in a
subclass that uses augment
, augride
, or
augment-final
. A method declared with augride
can
be overridden in a subclass that uses override
,
overment
, or override-final
. (Such an override
merely replaces the augmentation, not the method that is augmented.)
A method declared with augment-final
cannot be overridden or
augmented further in a subclass.
A method declared with private
is not accessible outside the
class expression, cannot be overridden, and never overrides a method
in the superclass.
When a method is declared with override
, overment
,
or override-final
, then the superclass implementation of the
method can be called using super
form:
(super identifier arg-expr ···)
Such a super
call always accesses the superclass method,
independent of whether the method is overridden again in subclasses.
When a method is declared with pubment
, augment
, or
overment
, then a subclass augmenting method can be called
using the inner
form:
(inner default-expr identifier arg-expr ···)
If the object's class does not supply an augmenting method, then
default-expr
is evaluated, and the arg-expr
s are not
evaluated. Otherwise, the augmenting method is called with the
arg-expr
results as arguments, and default-expr
is not
evaluated. If no inner
call is evaluated for a particular
method, then augmenting methods supplied by subclasses are never
used. (The only difference between public-final
and
pubment
without a corresponding inner
is that
public-final
prevents the declaration of augmenting methods
that would be ignored.)
6.3.3.2 Inherited and Superclass Methods
Each inherit
, inherit/super
,
inherit/inner
, rename-super
, and
rename-inner
clause declares one or more methods that are
defined in the class, but must be present in the superclass. The
rename-super
and rename-inner
declarations are
rarely used, since inherit/super
and inherit/inner
provide the same access. Also, superclass and augmenting methods are
typically accessed through super
and inner
in a
class that also declares the methods, instead of through
inherit/super
, inherit/inner
, rename-super
,
or rename-inner
.
Method names declared with inherit
, inherit/super
, or
inherit/inner
access overriding declarations, if any, at run
time. Method names declared with inherit/super
can also be used
with the super
form to access the superclass implementation,
and method names declared with inherit/inner
can also be used
with the inner
form to access an augmenting method, if any.
Method names declared with rename-super
always access the
superclass's implementation at run-time. Methods declared with
rename-inner
access a subclass's augmenting method, if any,
and must be called with the form
(identifier (lambda () default-expr) arg-expr ···)
so that a default-expr
is available to evaluate when no
augmenting method is available. In such a form, lambda
is a
keyword to separate the default-expr
from the
arg-expr
. When an augmenting method is available, it receives
the results of the arg-expr
s as arguments.
Methods that are present in the superclass but not declared with
inherit
, inherit/super
, or inherit/inner
or rename-super
are not directly accessible in the class
(through they can be called with send
). Every public method
in a superclass is present in a derived class, even if it is not
declared with inherit
in the derived class; the
inherit
clause does not control inheritance, but merely
controls lexical scope within a class expression.
If a method declared with inherit
, inherit/super
,
inherit/inner
, rename-super
, or
rename-inner
is not present in the superclass, the
exn:fail:object
exception is raised when the class expression is evaluated.
6.3.3.3 Internal and External Names
Each method declared with public
, override
,
augment
, pubment
, overment
,
augride
, public-final
,
override-final
, augment-final
, inherit
,
inherit/super
, inherit/inner
,
rename-super
, and rename-inner
can have separate
internal and external names when (internal-id
external-id)
is used for declaring the method. The internal
name is used to access the method directly within the class
expression (including within super
or inner
forms), while the external name is used with send
and
generic
(see section 6.5). If a single
identifier
is provided for a method declaration, the identifier is
used for both the internal and external names.
Method inheritance, overriding, and augmentation are based external
names, only. Separate internal and external names are required for
rename-super
and rename-inner
(for historical
reasons, mainly).
Each init
, init-field
, field
, or
inherit-field
variable similarly has an internal and an
external name. The internal name is used within the class to access
the variable, while the external name is used outside the class when
providing initialization arguments (e.g., to instantiate
),
inheriting a field, or accessing a field externally (e.g., with
class-field-accessor
). As for methods, when inheriting a field
with inherit-field
, the external name is matched to an
external field name in the superclass, while the internal name is
bound in the class
expression.
A single identifier can be used as an internal identifier and an external identifier, and it is possible to use the same identifier as internal and external identifiers for different bindings. Furthermore, within a single class, a single name can be used as an external method name, an external field name, and an external initialization argument name. Overall, each internal identifier must be distinct from all other internal identifiers, each external method name must be distinct from all other method names, each external field name must be distinct from all other field names, and each initialization argument name must be distinct from all other initialization argument names
By default, external names have no lexical scope, which means, for
example, that an external method name matches the same syntactic
symbol in all uses of send
. The
define-local-member-name
form introduces a set of scoped
external names:
(define-local-member-name identifier ···)
Unless it appears as the top-level definition, this form binds each
identifier
so that, within the scope of the definition, each use
of each identifier
as an external name is resolved to a hidden
name generated by the define-local-member-name
declaration. Thus, methods, fields, and initialization arguments
declared with such external-name identifier
s are accessible only
in the scope of the define-local-member-name
declaration.
As a top-level definition, define-local-member-name
binds
identifier
to its symbolic form.
The binding introduced by define-local-member-name
is a syntax
binding that can be exported and imported with modules (see
section 5 in PLT MzScheme: Language Manual). Each execution of a
define-local-member-name
declaration generates a distinct
hidden name (except as a top-level definitions). The
interface->method-names
procedure (see section 6.8)
does not expose hidden names.
Example:
(define o (let () (define-local-member-name m) (define c% (classobject%
(define/public (m) 10) (super-new)) (define o (new c%)) (send o m) ; =>10
o)) (send o m) ; => error: no methodm
The define-local-name
form maps a single external name to an
external name that is determined by an expression:
(define-member-name identifier key-expr)
The value of key-expr
must be the result of either a
member-name-key
expression,
(member-name-key identifier)
or (generate-member-key
)
. The latter produces a hidden name, just
like the binding for define-local-member-name
. The
(member-name-key identifier)
form produces a representation
of the external name for identifier
in the environment of the
member-name-key
expression. (member-name-key?
obj
)
returns
#t
for values produced by member-name-key
and
generate-member-key
, #f
otherwise. (member-name-key=?
a-key b-key
)
produces #t
if member-name keys a-key
and b-key
represent the same external name. (member-name-key-hash-code
a-key
)
produces an integer hash code consistent with member-name-key=?
comparsions, analogous to equal-hash-code
.
Example:
(define (make-c% key) (define-member-name m key) (classobject%
(define/public (m) 10) (super-new))) (send (new (make-c% (member-name-key m))) m) ; =>10
(send (new (make-c% (member-name-key p))) m) ; => error: no methodm
(send (new (make-c% (member-name-key p))) p) ; =>10
(define (fresh-c%) (let ([key (generate-member-name
)]) (values
(make-c% key) key))) (define-values (fc% key) (fresh-c%)) (send (new fc%) m) ; => error: no methodm
(let () (define-member-name p key) (send (new fc%) p)) ; =>10
When a class
expression is compiled, identifiers used in
place of external names must be symbolically distinct (when the
corresponding external names are required to be distinct), otherwise a syntax error
is reported. When no external name is bound by
define-member-name
, then the actual external names are
guaranteed to be distinct when class
expression is evaluated.
When any external name is bound by define-member-name
, the
exn:fail:object
exception is raised by class
if the actual external
names are not distinct.
6.4 Creating Objects
The make-object
procedure creates a new object with by-position
initialization arguments:
(make-object
class init-v
···)
An instance of class
is created, and the init-v
s are
passed as initialization arguments, bound to the initialization
variables of class
for the newly created object as described in
section 6.3.1. If class
is not a class, the
exn:fail:contract
exception is raised.
The new
form creates a new object with by-name
initialization arguments:
(new class-expr (identifier by-name-expr) ···)
An instance of the value of class-expr
is created, and
the value of each by-name-expr
is provided as a
by-name argument for the corresponding identifier
.
The instantiate
form creates a new object with both
by-position and by-name initialization arguments:
(instantiate class-expr (by-pos-expr ···) (identifier by-name-expr) ···)
An instance of the value of class-expr
is created, and the
values of the by-pos-expr
s are provided as by-position
initialization arguments. In addition, the value of each
by-name-expr
is provided as a by-name argument for the
corresponding identifier
.
All fields in the newly created object are initially bound to the
special undefined value (see
section 3.1 in PLT MzScheme: Language Manual). Initialization variables with default value
expressions (and no provided value) are also initialized to
undefined. After argument values are assigned to initialization
variables, expressions in field
clauses, init-field
clauses with no provided argument, init
clauses with no
provided argument, private field definitions, and other expressions
are evaluated. Those expressions are evaluated as they appear in the
class expression, from left to right.
Sometime during the evaluation of the expressions, superclass-declared
initializations must be executed once by using the
super-instantiate
form:
(super-instantiate (by-position-super-init-expr ···) (identifier by-name-super-init-expr ···) ···)
or by using the procedure produced by the super-make-object
form:
(super-make-object
super-init-v ···)
(super-new (identifier by-name-super-init-expr ···) ···)
The by-position-super-init-expr
s,
by-name-super-init-exp
s, and super-init-v
s are mapped to
initialization variables in the same way as for
instantiate
,
, and make-object
new
.
By-name initialization arguments to a class that have no matching
initialization variable are implicitly added as by-name arguments to
a super-instantiate
,
,
or super-make-object
super-new
invocation, after the explicit arguments.
If multiple initialization arguments are provided for the same name,
the first (if any) is used, and the unused arguments are propagated
to the superclass. (Note that converted by-position arguments are
always placed before explicit by-name arguments.) The initialization
procedure for the object%
class accepts zero initialization
arguments; if it receives any by-name initialization arguments, then
exn:fail:object
exception is raised.
Fields inherited from a superclass will not be initialized until the superclass's initialization procedure is invoked. In contrast, all methods are available for an object as soon as the object is created; the overriding of methods is not affect by initialization (unlike objects in C++).
It is an error to reach the end of initialization for any class in the
hierarchy without invoking superclasses initialization; the
exn:fail:object
exception is raised in such a case. Also, if superclass
initialization is invoked more than once, the exn:fail:object
exception is raised.
6.5 Field and Method Access
In expressions within a class definition, the initialization
variables, fields, and methods of the class all part of the
environment. Within a method body, only the fields and other methods
of the class can be referenced; a reference to any other
class-introduced identifier is a syntax error. Elsewhere within the
class, all class-introduced identifiers are available, and fields and
initialization variables can be mutated with
set!
.
6.5.1 Methods
Method names within a class can only be used in the procedure position of an application expression; any other use is a syntax error. To allow methods to be applied to lists of arguments, a method application can have the form
(method-id arg-expr ··· . arg-list-expr) (super method-id arg-expr ··· . arg-list-expr) (inner default-expr method-id arg-expr ··· . arg-list-expr)
which calls the method in a way analogous to (
. The
apply
method-id arg-expr ··· arg-list-expr)arg-list-expr
must not be a parenthesized expression, otherwise
the dot and the parentheses will cancel each other.
Methods are called from outside a class with the send
and send/apply
forms:
(send obj-expr method-name arg-expr ···) (send obj-expr method-name arg-expr ··· . arg-list-expr) (send/apply obj-expr method-name arg-expr ··· arg-list-expr)
where the last two forms apply the method to a list of argument
values; in the second form, arg-list-expr
cannot be a
parenthesized expression. For any send
or send/apply
,
if obj-expr
does not produce an object, the
exn:fail:contract
exception is raised. If the object has no public method
method-name
, the exn:fail:object
exception is raised.
The send*
form calls multiple methods of an object in the
specified order:
(send* obj-expr msg ···) msg is one of (method-name arg-expr ···) (method-name arg-expr ··· . arg-list-expr)
where arg-list-expr
is not a parenthesized expression.
Example:
(send* edit (begin-edit-sequence) (insert "Hello") (insert #\newline) (end-edit-sequence))
which is the same as
(let ([o edit]) (send o begin-edit-sequence) (send o insert "Hello") (send o insert #\newline) (send o end-edit-sequence))
The with-method
form extracts a method from an object and
binds a local name that can be applied directly (in the same way as
declared methods within a class):
(with-method ((identifier (object-expr method-name)) ···) expr ···1)
Example:
(let ([s (new stack%)]) (with-method ([push (s push!)] [pop (s pop!)]) (push 10) (push 9) (pop)))
which is the same as
(let ([s (new stack%)]) (send s push! 10) (send s push! 9) (send s pop!))
6.5.2 Fields
(get-field identifier object-expr)
extracts the field named by identifier
from the value of the
object-expr
.
(field-bound? identifier object-expr)
produces #t
if object-expr
evaluates to an object
that has a field named identifier
, #f
otherwise.
If you have access to the class of an object, the
class-field-accessor
and
class-field-mutator
forms provide efficient access to
the object's fields.
(class-field-accessor class-expr field-name)
returns an accessor procedure that takes an instance of the class produced byclass-expr
and returns the value of the object'sfield-name
field.(class-field-mutator class-expr field-name)
returns a mutator procedure that takes an instance of the class produced byclass-expr
and a new value for the field, mutates the field in the object named byfield-name
, then returns void.
6.5.3 Generics
A generic can be used instead of a method name to avoid the
cost of relocating a method by name within a class. The
make-generic
procedure and generic
form create
generics:
(make-generic
class-or-interface symbol
)
returns a generic that works on instances ofclass-or-interface
(or an instance of a class/interface derived fromclass-or-interface
) to call the method named bysymbol
.If
class-or-interface
does not contain a method with the (external and non-scoped) namesymbol
, theexn:fail:object
exception is raised.(generic class-or-interface-expr name)
is analogous to(make-generic
, except thatclass-or-interface-expr
'name
)name
can be a scoped method name declared bydefine-local-member-name
(see section 6.3.3.3).
A generic is applied with send-generic
:
(send-generic obj-expr generic-expr arg-expr ···) (send-generic obj-expr generic-expr arg-expr ··· . arg-list-expr)
where the value of obj-expr
is an object and the value of
generic-expr
is a generic.
6.6 Mixins
A mixin is a class parameterization modeled on a paper published by Flatt, Felleisen, and Krishnamurthi, available at http://www.ccs.neu.edu/scheme/pubs/#popl98-fkf.
The implementation of these mixins in MzScheme is with the combination of
lambda
and class
. This macro simplifies
the checking and implementation of these mixins. Its syntax is very
similar to the syntax for class*
. The shape of
a mixin is:
(mixin (interface-expr ...) (interface-expr ...) class-clause ...)
This macro expands into a procedure that accepts a
class. The argument passed to this procedure must match the
interfaces of the first interface-expr
s
expressions. The procedure returns a class that is derived
from its argument. This result class must match the
interfaces specified in the second interface-expr
s
section; it has clauses specified by
instance-variable-clause
s. The syntax of the initialization-variables
and
instance-variable-clause
are exactly the same as class*/names
.
The mixin
macro does some checking to be sure that
variables that the
instance-variable-clause
s refer to in their super class are in the
interfaces. That checking and the checking that the input class matches the
declared interfaces aside, the mixin macro's expansion is something
like this:
(mixin (i<%> ...) (j<%> ...) class-clause ...) = (lambda (%) (class* % (j<%> ...) class-clause ...))
The i<%>
interfaces do not appear in the output
because they are only used for the error checking and
are discarded by the time the class is created.
6.7 Object Serialization
The define-serializable-class
and define-serializable-class*
forms define classes
whose instances are serializable using serialize
(see section 43).
(define-serializable-class class-id superclass-expr class-clause ···) (define-serializable-class* class-id superclass-expr (interface-expr ···) class-clause ···)
These forms can only be used at the top level, either within a module
or outside. The class-id
identifier is bound to the new class,
and deserialize-info:
is also defined; if the
definition is within a module, then the latter is provided from the
module. The class-id
superclass-expr
, interface-expr
s,
and class-clause
s are as for class
and class*
(see section 6.3).
Serialization for the class works in one of two ways:
If the class implements the built-in interface
externalizable<%>
, then an object is serialized by calling itsexternalize
method; the result can be anything that is serializable (but, obviously, should not be the object itself). Deserialization creates an instance of the class with no initialization arguments, and then calls the object'sinternalize
method with the result ofexternalize
(or, more precisely, a deserialized version of the serialized result of a previous call). Theexternalizable<%>
interface includes only theexternalize
andinternalize
methods.To support this form of serialization, the class must be instantiable with no initialization arguments. Furthermore, cycles involving only instances of the class (and other such classes) cannot be serialized.
If the class does not implement
externalizable<%>
, then every superclass of the class must be either serializable or transparent (i.e,. have#f
as its inspector). Serialization and deserialization are fully automatic, and may involve cycles of instances.To support cycles of instances, deserialization may create an instance of the call with all fields as the undefined value, and then mutate the object to set the field values. Serialization support does not otherwise make an object's fields mutable.
In the second case, a serializable subclass can
implement externalizable<%>
, in which case
the externalize
method is responsible for all serialization
(i.e., automatic serialization is lost for instances of the
subclass). In the first case, all serializable subclasses
implement externalizable<%>
, since a subclass implements all
of the interfaces of its parent class.
In either case, if an object is an immediate instance of a subclass
(that is not itself serializable), the object is serialized as if it
was an immediate instance of the serializable class. In particular,
overriding declarations of the externalize
method are
ignored for instances of non-serializable subclasses.
6.8 Object, Class, and Interface Utilities
(object?
v
)
returns #t
if v
is an object, #f
otherwise.
(class?
v
)
returns #t
if v
is a class, #f
otherwise.
(interface?
v
)
returns #t
if v
is an interface,
#f
otherwise.
(object=?
object
[object
])
determines if two objects
are the same object, or not (uses
, but also
works properly with contracts).eq?
(object->vector
object
[opaque-v
])
returns a vector
representing object
that shows its inspectable fields,
analogous to struct->vector
(see section 4.9 in PLT MzScheme: Language Manual).
(class->interface
class
)
returns the interface implicitly defined
by class
(see the overview at the beginning of Chapter 6).
(object-interface
object
)
returns the interface implicitly
defined by the class of object
.
(is-a?
v interface
)
returns #t
if
v
is an instance of a class that implements interface
,
#f
otherwise.
(is-a?
v class
)
returns #t
if v
is
an instance of class
(or of a class derived from class
),
#f
otherwise.
(subclass?
v class
)
returns #t
if v
is a class
derived from (or equal to) class
, #f
otherwise.
(implementation?
v interface
)
returns #t
if v
is a
class that implements interface
, #f
otherwise.
(interface-extension?
v interface
)
returns #t
if v
is an interface that extends interface
, #f
otherwise.
(method-in-interface?
symbol interface
)
returns #t
if
interface
(or any of its ancestor interfaces) includes a member
with the name symbol
, #f
otherwise.
(interface->method-names
interface
)
returns a list of symbols for
the method names in interface
, including methods inherited from
superinterfaces, but not including methods whose names are local
(i.e., declared with define-local-member-names
).
(object-method-arity-includes?
object symbol k
)
returns #t
if object
has a method named symbol
that accepts k
arguments, #f
otherwise.
(field-names
object
)
returns a list of all of the names of the
fields bound in object
, including fields inherited from
superinterfaces, but not including fields whose names are local
(i.e., declared with define-local-member-names
).
(object-info
object
)
returns two values, analogous to the return
values of struct-info
(see section 4.5 in PLT MzScheme: Language Manual):
class
: a class or#f
; the result is#f
if the current inspector does not control any class for which theobject
is an instance.skipped?
:#f
if the first result corresponds to the most specific class ofobject
,#t
otherwise.
(class-info
class
)
returns seven values, analogous to the return
values of struct-type-info
(see section 4.5 in PLT MzScheme: Language Manual):
name-symbol
: the class's name as a symbol;field-k
: the number of fields (public and private) defined by the class;field-name-list
: a list of symbols corresponding to the class's public fields; this list can be larger thanfield-k
because it includes inherited fields;field-accessor-proc
: an accessor procedure for obtaining field values in instances of the class; the accessor takes an instance and a field index between0
(inclusive) andfield-k
(exclusive);field-mutator-proc
: a mutator procedure for modifying field values in instances of the class; the mutator takes an instance, a field index between0
(inclusive) andfield-k
(exclusive), and a new field value;super-class
: a class for the most specific ancestor of the given class that is controlled by the current inspector, or#f
if no ancestor is controlled by the current inspector;skipped?
:#f
if the sixth result is the most specific ancestor class,#t
otherwise.
6.9 Expanding to a Class Declaration
The class/derived
form is like class*
, but it
includes a sub-expression to use used as the source for all syntax
errors within the class definition. For
example, define-serializable-class
expands
to class/derived
so that error in the body of the class are
reported in terms of define-serializable-class
instead
of class
.
(class/derived original-datum (name-id super-expr (interface-expr ...) deserialize-id-expr) class-clause ···)
The original-datum
is the original expression to use for
reporting errors.
The name-id
is used to name the resulting class; if it
is #f
, the class name is inferred.
The super-expr
, interface-expr
s, and class-clause
s
are as for class*
(see section 6.3).
If the deserialize-id-expr
is not literally #f
, then a
serializable class is generated, and the result is two values instead
of one: the class and a deserialize-info structure produced
by make-deserialize-info
. The deserialize-id-expr
should produce a value suitable as the second argument
to make-serialize-info
, and it should refer to an export
whose value is the deserialize-info structure.
Future optional forms may be added to the sequence that currently ends
with deserialize-id-expr
.