Chapter 3 Object-oriented programming in Moby
Moby supports a rich class-based style of object-oriented
programming.
3.1 Objects
An object in Moby is a collection of fields (both mutable
and immutable) and methods.
The fields and methods of an object are called its members.
There are three operations on objects: select a field, update a field,
and invoke a method.
An object type specifies which members of an object
may be used by clients to operate on it.
Because of subtyping and hiding of members, an object can have different
types in different contexts.
Object types must be declared, much like datatypes, but subtyping is
structural.
For example, a type for one-dimensional point objects may be written
as follows:
objtype Point {
method getX : () -> Float
method move : Float -> Point
}
Such a Point object has no visible fields and two visible methods:
getX, which takes no arguments and returns a float,
and move, which takes a float and returns a point.
The intended semantics is that getX returns the point's
current position, while move shifts the location of the point by
some amount and returns the point.
Because move returns the point, we can chain together method
invocations.
For example, assuming that pt is a point, the expression:
pt.move(1.0).getX()
moves pt by 1.0 and then returns its new position.
Continuing the standard example, we might define color point objects
as follows:
objtype CPoint {
extends Point
method getC : () -> Color
method shade : Color -> CPoint
}
This declaration defines the CPoint type to be an object type that
includes all of the members of Point (with their given types), plus
two new methods: getC and shade.
It is shorthand for the following:
objtype CPoint {
method getX : () -> Float
method move : Float -> Point
method getC : () -> Color
method shade : Color -> CPoint
}
Note that the return type of the move method is still Point
(an not CPoint).1
3.2 Classes
In Moby, objects are implemented by classes.
For example, the following class is an implementation of the Point
object type.
class PointClass {
field x : var Float
public method getX () -> Float { self.x }
public method move (dx : Float) -> Point {
self.x := self.getX() + dx;
self
}
public maker point (x0 : Float) {
field x = x0
}
}
The body of this class lists four members.
The first is a field named x; we use the var keyword
to denote that x is a mutable field.
The second and third members are two methods named getX and
move. The final member is a maker function named
point.
Classes have two different kinds of clients: object
clients, who use the objects instantiated from the class, and
class clients, who extend the class through subclassing.
Typically, the interface available to class clients is richer than
that provided to object clients. Software engineering principles
suggest that access to members should be restricted by default.
To support this usage, Moby provides the keyword public.
Public members are available to both object and class clients, while
unannotated members are available only to class clients.
For example, because the x field in class PointClass
is not marked public, object clients cannot access it directly.
The full type of the objects implemented by this class
(i.e., the type available to deriving classes)
is equal
to the type:
objtype XPoint {
extends Point
field x : var Float
}
which exposes the mutable x field.
The last part of this class definition is the maker point.
Makers are special functions used to create objects.2
Unlike many class-based languages, we do not use the class name for
makers, but instead allow the programmer to specify maker names.
In general, a class may contain any number of makers.
The body of a maker is responsible for initializing all of the
fields of the object.
In this case, there is only the x field.
The Moby typing rules require that each maker
initialize all the fields defined in its class.
An object client of a class creates an object by using the new operator
to invoke one of the class's makers with an appropriate argument.
For example:
new point(0.0)
returns a new Point object located at the origin.
Note that the maker point is available without qualification;
it is visible because our minimal class mechanism does not define any
form of name-space structure --- that rôle is left to modules.
In addition to implementing objects, classes provide a vehicle for code
reuse via inheritance.
For example, we can implement color points by extending
PointClass:
class CPointClass {
inherits PointClass
field c : var Color
public override method move (dx : Float) -> Point {
super.move(2*dx)
}
public method getC () -> Color { self.c }
public method shade (dc : Color) -> CPoint {
self.c := self.getC().blend(dc);
self
}
public maker cpoint (x0 : Float, c0 : Color) {
super point(x0);
field c = c0
}
}
In this case, we call PointClass the superclass, and
CPointClass the subclass (or derived class).
There are three things to note about the CPointClass code.
First, the inherits clause specifies that the fields and methods
of PointClass are inherited by CPointClass.
Second, we override the move method to make color points speedy.
The use of the reserved identifier super in the body of the move
method statically binds the call of move to the PointClass
implementation.
Third, the cpoint maker invokes the point maker of its parent
class before initializing the c field.
3.3 Object construction and invariants
One important feature of class-based languages is support for the
establishment and maintenance of object and class-level
invariants.3
These guarantees are especially important to base-class implementors who want
to restrict how their classes may be extended.
Providing such support motivates several features of the Moby
class mechanism design.
Object-level invariants are established when the object is
initialized by its maker.
We require that all fields defined in a class be initialized
by each maker in the class and that subclasses always invoke a
superclass maker.
These requirements guarantee that fields are always initialized
before an object is used.
The author of a class can ensure that its invariants are maintained
by hiding the mutable state from subclasses (i.e., making the state private)
and by declaring methods to be final.
Unlike many class-based languages, we do not allow access to
the object's methods (via self) inside a maker.
This restriction avoids the complication of a superclass maker invoking
a subclass method before the subclass's fields have been
initialized.4
The main disadvantage of not being able to reference self inside
makers is that class-level invariants cannot be maintained when new
objects are created, since they require access to the new object.
Moby allows classes to include an initially
clause, which specifies an expression to be
evaluated each time an object is created.
This expression may refer to self.
The initially clauses are executed in the same order as
makers --- superclass and then subclass.
Thus, the act of creating a new object involves first computing
the initial values of its fields by invoking maker functions,
then creating the object and passing it to the initially clauses,
and finally returning the new object to the context that invoked
new.
3.4 typeof
As a syntactic convenience, Moby provides the typeof operator.
When applied to a class name, typeof provides a name for the
(public) object type associated with the class in the given scope.
For example, in the scope of the CPointClass above,
the type typeof(CPointClass) is equivalent to the type
CPoint.
3.5 Class interfaces
An interface is the type (or signature) of a class; it
is used when specifying a class in a module's signature.5
For example, the following module encapsulates the implementation
of the PointClass:
module Pt : {
class PointClass : {
implements Point
public maker point of Float
}
} { ... implementation of PointClass ... }
In this example, the Pt module is constrained by a signature
that specifies an interface for PointClass.
This interface specifies that PointClass provides an implementation of
the Point type (and hence must provide getX and
move methods with the appropriate types)
and a maker named point that takes a float
argument.
Matching the PointClass with this interface has the effect
of hiding the x field (i.e., inside the Pt module,
x is visible to deriving classes, but not outside).
Once a field or method is hidden in this way, subclasses are free to
define new members with the same name.
Note, however, that the original member is still accessible from
the superclass's methods (e.g., getX), even if its name is
reused.
The implements and maker clauses of a class interface
together capture the class's object view.
To avoid redundancy, Moby has programmers express class views as
incremental modifications to object views.
For example, we might encapsulate the CPointClass as follows:
module CPt : {
class CPointClass : {
implements CPoint
public maker cpoint of (Float, Color)
field c : var Color
public final method getC : () -> Color
}
} { ... implementation of CPointClass ...}
The field and method clauses of the CPointClass
interface specify
a refinement (or delta) of the object view.
Specifically, we have added a mutable field named c and noted
that the getC method is final (i.e., cannot be overridden by
subclasses).
In general, class interfaces may specify additional members and
makers, final annotations on methods, and refinements to the types of
methods, immutable fields, and makers.
As we have described, classes have two views: object
and class, and modules define two scopes: internal and external.
Combining these views gives us four distinct visibility modes,
which roughly correspond to those in C++ and Java as
follows:
|
Object view |
Class view |
External |
public |
protected |
Internal |
package |
private |
Nested modules permit even finer control over visibility.
3.6 Class types
Occasionally, it is useful to tie object types to specific classes.
Although it is possible to use abstract representation types
and representation methods to make this connection
classes, a more natural approach is to have
the class names themselves play the rôle of types
when the connection is needed.
To support this usage, Moby includes class types.
Syntactically, if C is a class name, then
#C is the corresponding class type. This type can
be used in any context that is in the scope of C. The
methods and fields available from an object with a given class type
depend on context: within a method body, the class type of the host
class permits access to all members in the class view, while outside
the class, it permits access only to public members.
The following code fragment illustrates the use of class types:
the BagM module uses class types to grant the union
function access to the items field of the Bag class,
essentially making union a friend of the class.
The signature for the BagM module then hides this field, so
code outside the BagM module cannot access the items
field directly. This use of class types ensures that the
union function can be passed only objects instantiated from
the Bag class or one of its descendants, guaranteeing that
the item field will be available.
module BagM : {
class Bag : {
public method add : Int -> ()
public maker mkBag of ()
}
val union : (#Bag, #Bag) -> ()
} {
class Bag {
public field items : var List(Int)
public method add (x : Int) -> () {
self.items := x :: self.items
}
public maker mkBag () { field items = Nil }
}
fun union (s1 : #Bag, s2 : #Bag) -> () {
List.app s1.add s2.items
}
}
When typing the methods of a class C, we give self the
type #C. Likewise, if B is C's
superclass, then super has the type #B.
Objects instantiated from class C are given type
#C.
To connect class types and object types, we have that for any class
C, #C <: typeof(C).
Subtyping between class types follows the class hierarchy, with one
side-condition. Namely, if class C inherits from class
B and typeof(C) <:
typeof(B), then we have that #C
<: #B.
Finally, to integrate class types and class interfaces, we extend
class interfaces to allow an optional inherits clause. If in a
given context a class C has an interface that includes an
``inherits B'' clause, then we view #C as
a subtype of #B. In this case, the type checker ensures
typeof(C) <: typeof(B) in determining that the
class interface for C is well-formed.
Omitting the inherits clause from
C's interface causes the relationship between B and
C to be hidden.
For example, in the following code fragment,
the signature for the CBagM module asserts that the
CBag class inherits from the BagM.Bag class.
Since typeof(CBag)
<:typeof(BagM.Bag), this class interface is
well-formed and we have the relationship that
#CBag
<: #BagM.Bag, which permits the
union function to be applied to objects instantiated from the
CBag class.
module CBagM : {
class CBag : {
inherits BagM.Bag
public method size : () -> Int
public maker mkCBag of ()
}
} {
class CBag {
inherits BagM.Bag
public field nItems : var Int
public override method add (x : Int) -> () {
self.nItems := self.nItems+1;
super.add(x)
}
public method size () -> Int { self.nItems }
public maker mkCBag () { super mkBag(); field nItems = 0 }
}
}
At first glance, the type typeof(C) may seem similar to
#C, since both types provide access to the same set of
members in a given context. The key difference is that objects derived
from classes completely unrelated to C may have the type
typeof(C), whereas an object of type #C
must have been generated from class C or one of its
descendants. In other words, typeof(C) is an
interface type and #C is an
implementation type.
3.7 Using tagtypes to implement checked down-casts
Using Moby's tagtype mechanism, programmers can provide a
type-safe downcast mechanism. For example, suppose we wished to allow
users of the Point object to downcast instantiated objects to
have type CPoint, if the object had the more refined type.
We first declare a tagtype that will
serve to indicate from which class a given object was instantiated:
tagtype PointKind of Point
We then add a method to the Point object to reveal its
associated tag:
objtype Point {
...
method kindOf : () -> PointKind
}
The implementation of the kindOf method wraps self with
the PointKind constructor:
class PointClass {
...
public method kindOf () -> PointKind { PointKind self }
}
When we define the ColorPoint class, we add a new constructor
to the PointKind tagtype:
tagtype CPointKind of CPoint extends PointKind
and override the kindOf method to wrap self with the new
constructor:
class CPointClass {
...
public override method kindOf () -> PointKind { CPointKind self }
}
With this infrastructure in place, we can write a function that
provides checked downcasting:
fun asCPt (pt : Point) -> Option(CPoint) {
case pt.kindOf() of
{ CPointKind cpt => Some cpt
, _ => None
}
}
3.8 Programming with classes and modules
The last example of this section is an idealized graphics application
that illustrates the interactions between classes and modules.
We use objects to represent the graphical elements of a picture,
which we call glyphs.
We start by defining a module signature that specifies the Glyph
type and a base class for implementing glyphs.
signature BASE_GLYPH {
class BaseGlyph : {
public final method draw : Point -> ()
abstract method drawGlyph : Drawable -> ()
maker mk of ()
}
}
where the type Drawable is the type of an object that represents
a drawing surface.
This signature specifies that the drawGlyph method is abstract;
thus we call BaseGlyph an abstract class.
Because it is an abstract class, its makers cannot be public and objects cannot
be created from the class.
Also, the class interface for BaseGlyph marks the draw method as
final, which means that derived classes cannot override or hide it.
Although no implementation is given here, the intuition is that method
draw sets up any conditions necessary for drawing, perhaps translates
the relevant coordinate system to the origin, etc., and then invokes the
protected drawGlyph method to do the actual drawing.
This design illustrates
method factoring: drawing code common to all glyphs is
factored into the draw method supplied by the base
class.
Since we might want to support different kinds of drawables (e.g.,
computer screens and postscript), we collect the subclasses of
BaseGlyph into a module that is parameterized by the
BASE_GLYPH signature:
module Glyphs (G : BASE_GLYPH) {
class LineGlyph {
inherits G.BaseGlyph
field p1 : Point
field p2 : Point
override method drawGlyph (d : Drawable) -> () { d.drawLine(p1, p2) }
public maker line (p1 : Point, p2 : Point) {
super mk();
field p1 = p1;
field p2 = p2
}
}
... implementations of other glyph classes ...
}
In this code fragment, LineGlyph is a subclass of G.BaseGlyph
and provides the implementation of the drawGlyph method for
drawing lines.
This example illustrates both module and class-based code reuse.
We use a parameterized module to abstract over the base-class
of LineGlyph, which allows multiple class hierarchies to
be defined by applying Glyphs to different base classes,
while using inheritance to factor out code common to all glyphs.
- 1
-
The return type of move does not change because Moby
has recursive object types, not
the more elaborate mytype
mechanism [BSv95, FM95, AC96].
Because our objects are stateful, we believe the extra power
of the mytype mechanism is not worth the additional complexity.
- 2
-
We use the term maker (borrowed from Theta [Pro95]),
instead of the more standard constructor, to avoid confusion with
data and type constructors.
- 3
-
Object-level invariants are properties of the state of a given object,
whereas class-level invariants are properties of a collection of objects
instantiated from a class.
- 4
-
C++ addresses this problem by changing the semantics
of method dispatch inside constructors, while Java relies
on the property that all fields are initialized to some
type-specific value prior to executing the constructor.
- 5
-
Moby's notion of an interface should not be confused with
that of Java.
Interfaces in Java play a role that is more closely related to
Moby's object types.