CS 5010 F '09
General
Lectures/Wand
Syllabus
Classes
Assignments
Presentations
Drill Club
Communication
Blog
Texts
FAQ
DrScheme

Classes in PLT

logo

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:

  • (class CLS 
       (init-field VRL ...)
       ELT ...)
       
  • (class* CLS (INAME ...)
       (init-field VRL ...)
       ELT ...)
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:

  • NAME
  • (NAME EXPRESSION)
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:

  1. the packaging of data definitions with structures and functions as classes; and
  2. 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))))


last updated on Wed Dec 2 12:06:07 EST 2009generated with PLT Scheme