Class Syntax
Creating a class is like creating a function. While
(lambda (NAME ...) EXPRESSION)
creates a function, a definition of the shape
(define FUNC-NAME (lambda (NAME ...) EXPRESSION))
introduces a name for a function. For the latter, you mostly use the
shorthand
(define (FUNC-NAME NAME ...) EXPRESSION)
but this really is just an abbreviation. The rest of this section
introduces the syntax for creating classes. For naming classes, use
define.
In the following explanations, whenever you see X ... read
"a possibly empty sequence of Xs".
A CLASS is one of:
where
CLS and
INAME are the names of classes
and interfaces, respectively. By convention all class names end in
% and all interface names end in
<%>.
A VRL is one of:
where
NAME is a variable name.
A FLD is (NAME EXPRESSION) where NAME is
the name of an init-field in the instantiated class.
An ELT is one of:
- (super-new FLD ...)
- (define/public (NAME NAME ...) EXPRESSION)
- (define/private (NAME NAME ...) EXPRESSION)
- (field VRL ...)
where
NAME is a variable name.
Interface Syntax
A INTERFACE is:
(interface (INAME ...) NAME)
where
INAME is the name of an interface and
NAME
is the name of a method.
Expression Syntax
Classes are used to create objects, which are first-class values just
like functions. In contrast to functions, objects can be called in many
different ways, one per public method in its class. This two
new forms of syntax are specified in this section.
An EXPRESSION is one of:
- ... all the things you know
- (new CLS FLD ...)
- (send EXPRESSION NAME EXPRESSION ...)
- (is-a? EXPRESSION CLS)
where
CLS is a class name (actually, an expression that
evaluates to a class) and
NAME is the name of a public
method.
Pragmatics
Until now, we have represented a Cartesian point as an instance of posn.
Now we can use classes for the same purpose.
| a data representation for Cartesian points |
|---|
|
structure and class definitions
| (define-struct posn (x y))
| (define posn%
(class object%
(init-field x y)
(define/public (get-x) x)
(define/public (get-y) y)
(super-new)))
|
|
data definitions
| (define-struct posn (x y))
;; Posn = (make-posn Number Number)
;; interpretation: (make-posn x y)
;; is x pixels from the left
;; and y pixels from the top
| ;; a point on a 2D canvas
(define posn%
(class object%
(init-field
x ;; Number, pixels from left
y) ;; Number, pixels from top
(define/public (get-x) x)
(define/public (get-y) y)
(super-new)))
|
| creating examples | (define ex-s
(make-posn 3 4))
| (define ex-c
(new posn% [x 3][y 4]))
|
| extracting values | (posn-x ex-s) | (send ex-c get-x) |
| (posn-y ex-s) | (send ex-c get-y) |
| predicates | (posn? ex-s) | (is-a? ex-c posn%) |
| adding functionality | ;; Posn Posn -> Posn
;; add the given Posns, pointwise
(define (posn+ this p)
(make-posn
(+ (posn-x this) (posn-x p))
(+ (posn-y this) (posn-y p))))
| (define posn%
(class object%
(init-field x y)
(define/public (get-x) x)
(define/public (get-y) y)
;; posn% -> posn%
;; add this to p, pointwise
(define/public (posn+ p)
(new posn%
[x (+ x (send p get-x))]
[y (+ y (send p get-y))]))
(super-new)))
|
| testing | (check-expect
(posn+ ex-s (make-posn 0 0))
ex-s)
| (define O
(new posn% [x 0][y 0]))
(check-expect
(send
(send ex-c posn+ O)
get-x)
(send ex-c get-x))
(check-expect
(send
(send ex-c posn+ O)
get-y)
(send ex-c get-y))
;; abstract to:
(check-expect
(local ((define n
(send ex-c posn+ O)))
(list
(send n get-x)
(send n get-y)))
(list
(send ex-c get-x)
(send ex-c get-y)))
|
Some comments on this code, starting with the row dubbed "data
definitions". Just like the teaching languages, the Module language does
not impose any restrictions on what values a variable may represent. To
design programs it is therefore necessary to explain to future readers
how classes should be used. Here we see that the creation of an object
from posn% must supply two numbers, one for the
x field and one for the y field. The
interpretation of the fields is supplied as a comment, too.
The creation of object examples (also called "instances" of the class)
employs a by-label protocol, meaning the values for the fields of an
object are specified via a label not via a position in some argument
list. While this protocol is more verbose than by-position, it is
definitely easier to understand--especially when some fields come with
default values and aren't even mentioned in the argument list.
In a class-based world of design, you use methods not
functions to express functionality. It is understood that methods always
consume one implicit argument, dubbed this, and however
many additional explicit arguments as needed. For these additional
arguments, you must specify a contract--just as before in BSL/ISL. The
purpose statement is always formulated in terms of this
object, its fields, and the remaining arguments. A method accesses the
field values of the implicit this argument via the names
of the fields. See the red x and y. For the
additional arguments, the method must use method calls (also known as
"send a message") to obtain knowledge about the internals. Here the
posn+ method uses the get-x and
get-y methods to obtain the coordinates. Last but not least,
notice the blue occurrences of x and y. These
are NOT variables, but the externally visible labels for the initial
fields of posn% objects.
When it comes to testing, class-based programming occasionally imposes an
additional overhead. You might expect that
(check-expect (send ex-c posn+ O) ex-c)
might work as before, but this is not the case. It is impossible (for you)
to compare objects in this straightforward manner. You must instead
use methods to extract basic values (numbers, booleans, strings, symbols,
images, or instances of structs) and then compare those.
Last not but least, the above code doesn't run in Module per se. You also
need to require the unit testing library explicitly:
(require test-engine/scheme-tests)
and then call
(test)
at the end of the file explicitly. To see which parts of your module the
tests (don't) cover, open the Language Dialog, look at the Details and
click the check box for "syntactic test suite coverage".
Design I
The basic purpose of a class system is to combine structures and functions
on structures into one coherent whole. It is thus orthogonal to the design
of programs per se. When you develop a data representation, you still need
to understand whether
- it represents basic forms of data
- or simple compounds, like structures
- or itemizations of different sub-classes of data
- or mutually referential/self-referential itemizations
and so on. Let's quickly check how to deal with itemizations of
subclasses of data, first the simple case and then the self-referential
case.
Consider a data representation for geometric shapes, which should include
circles and squares and possibly other forms of shapes. In addition, say
we wish to compute the area of these shapes and render them into
pre-existing scenes. A representation appropriate for programming with
classes looks like this:
;; all geometric shapes support these methods in all contexts
(define shape<%>
(interface ()
area ;; -> Number
;; compute the area of this shape
render ;; Scene -> Scene
;; add this shape to the given scene
))
;; a circle
(define circle%
(class* object% (shape<%>)
(init-field x ; Number, x pixels of center from left
y ; Number, y pixels of center from top
r ; Number, radius
c) ; ColorString
(define/public (area) (* pi r r))
(define/public (render s) (place-image IMG x y s))
(field [IMG (circle r "solid" c)])
(super-new)))
;; a square parallel to sides of canvas
(define square%
(class* object% (shape<%>)
(init-field x ; Number, x pixels of upper-left from left
y ; Number, y pixels of upper-left from top
l ; Number, length of one side
c) ; ColorString
(define/public (area) (* l l))
(define/public (render s) (place-image IMG x y s))
(field [IMG (rectangle l l "solid" c)])
(super-new)))
(define s1 (new circle% [x 10][y 20][r 40][c "red"]))
(define s2 (new square% [x 10][y 20][l 40][c "red"]))
(check-within (send s1 area) (* pi 1600) .1)
(check-expect (send s2 area) 1600)
Note the purpose statements of the interfaces and classes as well as the
methods in the interfaces. The method definitions per se do not need such
comments anymore. Because of the "implements" specification of the
class* form, a reader knows that the contract and purpose
statement are found in the interface. Do run this code in drscheme and
determine for yourself what the tests cover.
One kind of data you may wish to represent composites of geometric
shape. This could take many forms, so let's focus here on one where one
geometric shape is superimposed graphically onto another. In an HtDP-style
data definition, you would write something akin to this:
A shape is one of:
- a circle
- a square
- a combination of two shapes, with one specified as the "top" and the
other one to be the "bottom" shape.
To implement it you would translate this description of information into
structure definitions with a data definition that expresses the above with
your chosen constructors. The self-reference from above would become a
self-reference in the semi-formal English definition.
In the world of classes and interfaces, some of this relationship is
expressed via the "implements" clause and with some more English:
;; two overlapping shapes
(define overlay%
(class* object% (shape<%>)
(init-field top ; shape<%>, the shape on top
bot) ; shape<%>, the shape underneath
(define/public (render s)
(send top render (send bot render s)))
(define/public (area)
(+ (send top area) (send bot area)))
(super-new)))
As you can see, we no longer have a data definition in one place, but the
comments on the
init-fields tells readers that the data
representation is self-referential. Not surprisingly, the two methods in
this class are recursive in that they use the
render and
area methods, respectively, for their fields to compute the
appropriate results. We know that the values in the fields (are expected
to) have such methods from the data definition comments on the fields.
You may also have noticed that the addition of overlay% did
not require any modification of either the data representation per se or
the methods in other classes. This property of a class-based
object-oriented organization of programs--design as before with a
structural strategy--is dubbed "extensibility" and is often touted as an
advantage of object-oriented programming. It is the result of two
properties of class-based languages:
-
the packaging of data definitions with structures and functions as
classes; and
-
the encapsulation of the "selection" process in the
send
command. That is, send checks the kind of object on which a
method is invoked and then evaluate the appropriate method directly. In a
functional approach, this selection is a part of the conditional
expression that a function from shapes to areas or scenes would have to
include.
For details on these ideas, see "How to Design Classes", which presents
the very same ideas using the Java notation.
Design II
Why it matters to think about immutability:
The functional approach may appear unnatural if you're not familiar with
it, but it enables immutability, which has many advantages. Immutable
objects are simple. An immutable object can be in exactly one state, the
state in which it was created. If you make sure that all constructors
establish class invariants, then it is guaranteed that these invariants
will remain true for all time, with no further effort on your part or on
the part of the programmer who uses the class. Mutable objects, on the
other hand, can have arbitrarily complex state spaces. If the
documentation does not provide a precise description of the state
transitions performed by mutator methods, it can be difficult or impossible
to use a mutable class reliably.
Joshua Bloch, item 15, entitled "Minimize Mutability" (page 75).
Bloch is the architect of the Java API.
stateful classes, imperative methods
Design III
(define segment%
(class object%
(init-field color x y) ;; Color Nat Nat
;; Scene -> Scene
;; add an image of this segment to the given scene s
(define/public (render s)
(place-image (send this shape color) x y s))
;; Color -> Image
;; create an image of this segment
(define/public (shape c)
(error 'shape "is an abstract method in segment%"))
(super-new)))
(define head%
(class segment%
(super-new [color 'red])
(define/override (shape c)
(circle 3 'solid c))))
(define body%
(class segment%
(super-new [color 'green])
(define/override (shape c)
(rectangle 3 3 'solid c))))