1 Welcome to PLT Scheme
2 Scheme Essentials
3 Built-In Datatypes
4 Expressions and Definitions
5 Programmer-Defined Datatypes
6 Modules
7 Contracts
8 Input and Output
9 Regular Expressions
10 Exceptions and Control
11 Iterations and Comprehensions
12 Pattern Matching
13 Classes and Objects
14 Units (Components)
15 Reflection and Dynamic Evaluation
16 Macros
17 Performance
18 Running and Creating Executables
19 Compilation and Configuration
20 More Libraries
Bibliography
Index
On this page:
13.1 Methods
13.2 Initialization Arguments
13.3 Internal and External Names
13.4 Interfaces
13.5 Final, Augment, and Inner
13.6 Controlling the Scope of External Names
13.7 Mixins
13.7.1 Mixins and Interfaces
13.7.2 The mixin Form
13.7.3 Parameterized Mixins
13.8 Traits
13.8.1 Traits as Sets of Mixins
13.8.2 Inherit and Super in Traits
13.8.3 The trait Form
Version: 4.0.2

 

13 Classes and Objects

This chapter is based on a paper [Flatt06].

A class expression denotes a first-class value, just like a lambda expression:

(class superclass-expr decl-or-expr ...)

The superclass-expr determines the superclass for the new class. Each 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 %. The built-in root class is object%. The following expression creates a class with public methods get-size, grow, and eat:

  (class object%

    (init size)                ; initialization argument

  

    (define current-size size) ; field

  

    (super-new)                ; 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))))

The size initialization argument must be supplied via a named argument when instantiating the class through the new form:

  (new (class object% (init size) ....) [size 10])

Of course, we can also name the class and its instance:

  (define fish% (class object% (init size) ....))

  (define charlie (new fish% [size 10]))

In the definition of fish%, current-size is a private field that starts out with the value of the size initialization argument. Initialization arguments like size are available only during class instantiation, so they cannot be referenced directly from a method. The current-size field, in contrast, is available to methods.

The (super-new) expression in fish% invokes the initialization of the superclass. In this case, the superclass is object%, which takes no initialization arguments and performs no work; super-new must be used, anyway, because a class must always invoke its superclass’s initialization.

Initialization arguments, field declarations, and expressions such as (super-new) can appear in any order within a 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 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.

13.1 Methods

Each of the three define/public declarations in 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 grow method of a fish% object requires the send form:

  > (send charlie grow 6)

  > (send charlie get-size)

  16

Within fish%, self methods can be called like functions, because the method names are in scope. For example, the eat method within fish% directly invokes the 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 send with this to access the method:

  (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 inherit, which brings the method name into scope for a direct call:

  (define hungry-fish% (class fish% (super-new)

                         (inherit eat)

                         (define/public (eat-more fish1 fish2)

                           (eat fish1) (eat fish2))))

With the inherit declaration, if fish% had not provided an eat method, an error would be signaled in the evaluation of the class form for hungry-fish%. In contrast, with (send this ....), an error would not be signaled until the eat-more method is called and the send form is evaluated. For this reason, inherit is preferred.

Another drawback of send is that it is less efficient than inherit. Invocation of a method via send involves finding a method in the target object’s class at run time, making send comparable to an interface-based method call in Java. In contrast, 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 inherit-based method calls when invoking a method from outside the method’s class, the programmer must use the generic form, which produces a class- and method-specific generic method to be invoked with send-generic:

  (define get-fish-size (generic fish% get-size))

  > (send-generic charlie get-fish-size)

  16

  > (send-generic (new hungry-fish% [size 32]) get-fish-size)

  32

  > (send-generic (new object%) get-fish-size)

  generic:get-size for class: fish%: expected argument of

  type <instance for class: fish%>; given #(struct:object)

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 class, through a generic method, or through send, method overriding works in the usual way:

  (define picky-fish% (class fish% (super-new)

                        (define/override (grow amt)

  

                          (super grow (* 3/4 amt)))))

  (define daisy (new picky-fish% [size 20]))

  > (send daisy eat charlie)

  > (send daisy get-size)

  32

The grow method in picky-fish% is declared with define/override instead of define/public, because grow is meant as an overriding declaration. If grow had been declared with define/public, an error would have been signaled when evaluating the class expression, because fish% already supplies grow.

Using define/override also allows the invocation of the overridden method via a super call. For example, the grow implementation in picky-fish% uses super to delegate to the superclass implementation.

13.2 Initialization Arguments

Since picky-fish% declares no initialization arguments, any initialization values supplied in (new picky-fish% ....) are propagated to the superclass initialization, i.e., to fish%. A subclass can supply additional initialization arguments for its superclass in a super-new call, and such initialization arguments take precedence over arguments supplied to new. For example, the following size-10-fish% class always generates fish of size 10:

  (define size-10-fish% (class fish% (super-new [size 10])))

  > (send (new size-10-fish%) get-size)

  10

In the case of size-10-fish%, supplying a size initialization argument with new would result in an initialization error; because the size in super-new takes precedence, a size supplied to new would have no target declaration.

An initialization argument is optional if the class form declares a default value. For example, the following default-10-fish% class accepts a size initialization argument, but its value defaults to 10 if no value is supplied on instantiation:

  (define default-10-fish% (class fish%

                             (init [size 10])

                             (super-new [size size])))

  > (new default-10-fish%)

  #(struct:object:default-10-fish% ...)

  > (new default-10-fish% [size 20])

  #(struct:object:default-10-fish% ...)

In this example, the super-new call propagates its own size value as the size initialization argument to the superclass.

13.3 Internal and External Names

The two uses of size in default-10-fish% expose the double life of class-member identifiers. When size is the first identifier of a bracketed pair in new or super-new, size is an external name that is symbolically matched to an initialization argument in a class. When size appears as an expression within default-10-fish%, size is an internal name that is lexically scoped. Similarly, a call to an inherited eat method uses eat as an internal name, whereas a send of eat uses eat as an external name.

The full syntax of the 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.

13.4 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 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.

(interface (superinterface-expr ...) id ...)

To declare that a class implements an interface, the class* form must be used instead of class:

(class* superclass-expr (interface-expr ...) decl-or-expr ...)

For example, instead of forcing all fish classes to be derived from fish%, we can define fish-interface and change the fish% class to declare that it implements fish-interface:

  (define fish-interface (interface () get-size grow eat))

  (define fish% (class* object% (fish-interface) ....))

If the definition of fish% does not include get-size, grow, and eat methods, then an error is signaled in the evaluation of the class* form, because implementing the fish-interface interface requires those methods.

The is-a? predicate accepts either a class or interface as its first argument and an object as its second argument. When given a class, is-a? checks whether the object is an instance of that class or a derived class. When given an interface, is-a? checks whether the object’s class implements the interface. In addition, the implementation? predicate checks whether a given class implements a given interface.

13.5 Final, Augment, and Inner

As in Java, a method in a class form can be specified as final, which means that a subclass cannot override the method. A final method is declared using public-final or 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 augmentable methods [Goldberg04]. A method declared with pubment is like public, but the method cannot be overridden in subclasses; it can be augmented only. A pubment method must explicitly invoke an augmentation (if any) using inner; a subclass augments the method using augment, instead of override.

In general, a method can switch between augment and override modes in a class derivation. The 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, overment overrides a method and makes the overriding implementation augmentable.

13.6 Controlling the Scope of External Names

As noted in Internal and External Names, 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 member key; a class ultimately maps member keys to methods, fields, and initialization arguments.

Recall the hungry-fish% class expression:

  (define hungry-fish% (class fish% ....

                         (inherit eat)

                         (define/public (eat-more fish1 fish2)

                           (eat fish1) (eat fish2))))

During its evaluation, the hungry-fish% and fish% classes refer to the same global binding of eat. At run time, calls to eat in hungry-fish% are matched with the eat method in fish% through the shared method key that is bound to eat.

The default binding for an external name is global, but a programmer can introduce an external-name binding with the define-member-name form.

(define-member-name id member-key-expr)

In particular, by using (generate-member-key) as the 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, 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 fish% and pond% classes cooperate via a get-depth method that is only accessible to the cooperating classes:

  (define-values (fish% pond%) ; two mutually recursive classes

    (let ()

      (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 send, for initialization-argument names in new, or for the external name in a member definition. The special form member-name-key provides access to the binding of an external name in an arbitrary expression position: (member-name-key id) produces the member-key binding of id in the current scope.

A member-key value is primarily used with a define-member-name form. Normally, then, (member-name-key id) captures the method key of id so that it can be communicated to a use of define-member-name in a different scope. This capability turns out to be useful for generalizing mixins, as discussed next.

13.7 Mixins

Since class is an expression form instead of a top-level declaration as in Smalltalk and Java, a class form can be nested inside any lexical scope, including lambda. The result is a mixin, i.e., a class extension that is parameterized with respect to its superclass.

For example, we can parameterize the picky-fish% class over its superclass to define picky-mixin:

  (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 define/override makes explicit that picky-mixin expects a class with a grow method. If picky-mixin is applied to a class without a grow method, an error is signaled as soon as picky-mixin is applied.

Similarly, a use of inherit enforces a “method existence” requirement when the mixin is applied:

  (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 picky-mixin and hungry-mixin, creating a class for a hungry, yet picky fish is straightforward:

  (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, picky-mixin and hungry-mixin can augment any class with suitable eat and grow methods, because they do not specify initialization arguments and add none in their super-new expressions:

  (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 picky-mixin to person% works because the names eat and grow match, without any a priori declaration that eat and grow should be the same method in fish% and 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 Controlling the Scope of External Names.

13.7.1 Mixins and Interfaces

Using implementation?, picky-mixin could require that its base class implements grower-interface, which could be implemented by both fish% and person%:

  (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, is-a? cannot work on a mixin represented as a function, but it can recognize an interface (somewhat like a specialization interface) that is consistently implemented by the mixin. For example, classes generated by picky-mixin could be tagged with picky-interface, enabling the is-picky? predicate:

  (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))

13.7.2 The mixin Form

To codify the lambda-plus-class pattern for implementing mixins, including the use of interfaces for the domain and range of the mixin, the class system provides a mixin macro:

(mixin (interface-expr ...) (interface-expr ...)

  decl-or-expr ...)

The first set of interface-exprs 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 interface-exprs and produces a class that implements the second sequence of interface-exprs. Other requirements, such as the presence of inherited methods in the superclass, are then checked for the class expansion of the 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 Final, Augment, and Inner).

13.7.3 Parameterized Mixins

As noted in Controlling the Scope of External Names, external names can be bound with 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 hungry-mixin with respect to the external member key for eat:

  (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 eat method, which we can obtain using member-name-key:

  ((make-hungry-mixin (member-name-key eat))

   (class object% .... (define/public (eat x) 'yum)))

Above, we apply hungry-mixin to an anonymous class that provides eat, but we can also combine it with a class that provides chomp, instead:

  ((make-hungry-mixin (member-name-key chomp))

   (class object% .... (define/public (chomp x) 'yum)))

13.8 Traits

A 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 trait-sum (merge the methods of two traits), trait-exclude (remove a method from a trait), and 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 fish% programmer wants to define two class extensions, spots and stripes, each of which includes a get-color method. The fish’s spot color should not override the stripe color nor vice-versa; instead, a spots+stripes-fish% should combine the two colors, which is not possible if spots and stripes are implemented as plain mixins. If, however, spots and stripes are implemented as traits, they can be combined. First, we alias get-color in each trait to a non-conflicting name. Second, the 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 get-color method based on the two aliases, producing the desired spots+stripes extension.

13.8.1 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:

  (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 trait-sum and trait-exclude as simple manipulations; unfortunately, it does not support the trait-alias operator. Although a mixin can be duplicated in the association list, the mixin has a fixed method name, e.g., get-color, and mixins do not support a method-rename operation. To support trait-alias, we must parameterize the mixins over the external method name in the same way that eat was parameterized in Parameterized Mixins.

To support the trait-alias operation, spots-trait should be represented as:

  (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 get-color method in spots-trait is aliased to get-trait-color and the get-color method is removed, the resulting trait is the same as

  (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 T to a class C and obtain a derived class, we use ((trait->mixin T) C). The trait->mixin function supplies each mixin of T with the key for the mixin’s method and a partial extension of C:

  (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 get-color becomes a reference to the external name get-trait-color.

13.8.2 Inherit and Super in Traits

This first implementation of traits supports 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:

  (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 spots-trait fails, because get-color is not in scope for the get-price mixin. Indeed, depending on the order of mixin application when the trait is applied to a class, the get-color method may not be available when get-price mixin is applied to the class. Therefore adding an (inherit get-color) declaration to the get-price mixin does not solve the problem.

One solution is to require the use of (send this get-color) in methods such as get-price. This change works because 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 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 inherit to directly access any introduced method.

  (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, trait-alias adds a new method with a new name, but it does not change any references to the old method.

13.8.3 The trait Form

The general-purpose trait pattern is clearly too complex for a programmer to use directly, but it is easily codified in a trait macro:

(trait trait-clause ...)

The ids in the optional inherit clause are available for direct reference in the method exprs, 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 trait-sum, trait-exclude, trait-alias, and trait->mixin, we can implement spots-trait and stripes-trait as desired.

  (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) ....))))