Previous Up Next

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.

Previous Up Next