• No results found

new coordinates.

# letnew_imp_poly vertices = object (self : ’self)

val mutable float_vertices = [||] val mutable int_vertices = [||]

method draw = Graphics.fill_poly int_vertices

method transform matrix = {< >}#transform_in_place matrix method transform_in_place matrix =

self#set_vertices (Array.map matrix#transform vertices) method private set_vertices vertices =

float_vertices <- vertices;

int_vertices <- Array.map int_coord float_vertices initializer

self#set_vertices vertices end;;

val new_imp_poly : (float * float) array -> < draw : unit;

transform : (< transform : float * float -> float * float; .. > as ’a) -> unit; transform_in_place : ’a -> unit > = <fun>

The methodset_verticesis called in two places, 1) by the initializerto set the

initial values of the vertices, and 2) by the methodtransform_in_place, which com-

putes new values for the vertices. The object referenceselfis legal in the initializer,

allowing the method callself#set_vertices.

This example also contains a private method. Private methods are defined with the syntaxmethod private identifier = expression. They are used just like normal

(public) methods, but they are not visible outside the object—they don’t even appear in the object type.

14.8 Object types, coercions, and subtyping

The types of the objects we have been creating are getting pretty complicated. To make sense of it all, let’s make some type definitions. We don’t really care about giving the most general polymorphic types, so let’s use exact non-polymorphic types instead. We’ll call a drawable object ablob.

typecoord = float * float

typetransform = < transform : coord -> coord >

typeblob = < draw : unit; transform : transform -> blob > typecollection =

< add : blob -> unit; draw : unit;

transform : transform -> collection >

Note that the typecollectiondiffers fromblobin two ways. A collection has an extra

methodadd, and the methodtransformreturns another collection, not a blob.

We can now annotate the object creation functions to get simpler types (the object definitions are the same as before).

14.8. OBJECT TYPES, COERCIONS, AND SUBTYPINGCHAPTER 14. OBJECTS # let new_poly (vertices : coord array) : blob =object · · ·end;;

val new_poly : coord array -> blob = <fun>

# let new_circle (center : coord) radius : blob =object · · · end;;

val new_circle : coord -> float -> blob = <fun>

# let new_collection () : collection = object · · · end;;

val new_collection : unit -> collection = <fun>

Now that the types are simplified, we run into a new issue: the actual object types do not match the expected exact types. For example, the methodtransformnow expects an object with exactly one method (the method is also calledtransform), but the real object has many more methods. Here is what we get if we try to perform a transforma- tion.

# let circle = new_circle (0.0, 0.0) 0.1;;

val circle : blob = <obj>

# circle#transform (transform#new_translate 100.0 100.0);;

Characters 17-54:

circle#transform (transform#new_translate 100.0 100.0);; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This expression has type

< multiply : ... > as ’a

but is here used with type transform

The second object type has no method multiply

The problem here is that the expression(transform#new_translate 100.0 100.0)

produces an actual object with many methods, while the methodcircle#transform

expects an object having exactly one method. In principle, it should be fine to pass an object with more methods to one that expects fewer—all the extra methods can simply be ignored.

14.8.1 Coercions

In OCaml, such coercions are in fact legal, but they are not automatic. This is in accord with the usual OCaml policy that all coercions should be explicit; for example, integers are never coerced automatically to floating-point values, the functionfloat_of_int

must be written explicitly.

An explicit object coercion can be written two ways, as a “single coercion” or as a “double coercion.”

(object :> object-type) single coercion

(object : object-type :> object-type) double coercion

The single coercion expression(e :> t)coerces the objecteto have typet(if legal). The double coercion expression(e : t1 :> t2)means to consider first thatehas type

t1, then coerce it to an object of typet2. In most cases, a single coercion is sufficient. # circle#transform (transform#new_translate 100.0 100.0 :> transform);;

- : blob = <obj>

For another example, consider the collection object, which contains a list of simple blobs. If we want to add any other kind of object, it must be coerced.

CHAPTER 14. OBJECTS14.8. OBJECT TYPES, COERCIONS, AND SUBTYPING # letimage = new_collection ();;

val image : collection = <obj>

#image#add (new_circle (0.0, 0.0) 0.1);; - : unit = () #image#add star;; Characters 10-14: image#add star;; ^^^^

This expression has type collection but is here used with type blob The second object type has no method add

That is as we expected, but the single coercion doesn’t work either. #image#add (star :> blob);;

Characters 11-15:

image#add (star :> blob);; ^^^^

This expression cannot be coerced to type

blob = < draw : unit; transform : transform -> blob >; ...

This simple coercion was not fully general. Consider using a double coercion.

The real error message is quite long, most of it has been elided. The last line suggests using a double coercion, so we try that. Finally, success.

#star;;

- : collection = <obj>

#image#add (star : collection :> blob);;

- : unit = ()

Why does the single coercion sometimes work, but at other times the double coercion is required? The complete technical explanation is complicated and has to do with the specific algorithm used for type inference. The simplest rule is this: if the compiler complains about a single coercion, try replacing it with a double coercion. If you would like to know the real reason, a bit of explanation might be helpful.

Internally, the compiler uses only double coercions. Whenever the compiler en- counters a single coercion(e :> t2)it constructs a double coercion(e : t1 :> t2) by inferring themost general expected typet1. However, this fails if there is no unique most general expected type. The general guidelines can be stated as follows.

A single coercion(e :> t2)may fail if: • the typet2is recursive, or

• the typet2has polymorphic structure.

If either condition holds, use a double coercion(e : t1 :> t2).

In our example, the single coercion (transform#new_translate 100.0 100.0 :> transform)is successful because the type transformis neither recursive nor poly- morphic. However, the coercion(star :> blob)fails because the typeblobis recur- sive. The compiler doesn’t consider the actual type of the expression, so even though we knowstarhas typecollection, it is still necessary to write the double coercion (star : collection :> blob).

14.8. OBJECT TYPES, COERCIONS, AND SUBTYPINGCHAPTER 14. OBJECTS

14.8.2 Subtyping

That’s not the entire story of course, because not all coercions (e : t1 :> t2) are legal. There are two necessary conditions: first, expressioneshould have typet1; and second, typet1must be a subtype oft2.

We say that a typet1 is asubtypeoft2, writtent1 <: t2, if values of typet1 can be used where values of typet2are expected. It may be confusing that the symbols:> (the coercion operator) and <: (the subtyping relation) look like they point in opposite directions. It may be helpful to remember that the former is an operator, and the latter is a relation not belonging to the syntax of the language.

Consider the following type definitions: ananimaleats, and adogalso barks. type animal = < eat : unit >

type dog = < eat : unit; bark : unit >

The subtyping relationdog<:animalholds because adogobject has all the methods that an animal has with the same type, and so a dog objectecan be used wherever an

animalobject is expected (so the coercion(e : dog :> animal)is legal). Width and depth subtyping

Subtyping for object types takes two forms, calledwidthanddepthsubtyping. Width subtyping means that an object typet1is a subtype of object typet2ift1implements all the methods (and possibly more) oft2 with the same method types. The order of methods in an object type doesn’t matter, so we can write this as follows, where we use the notationf1..n : t1..n to representnmethod declarationsfi : ti fori

{1,2, . . . , n}.

< f1..n :t1..n,g1..m :s1..m > <: < f1..n : t1..n >

The subtyping relationdog <: animalfollows from width subtyping, because class

typedogimplements theeatmethod, the only method in theanimalclass type.

< eat : unit; bark : unit > <: < eat : unit >

Depth subtyping means that an object typet1is a subtype oft2if the two types have the same methods, but the method types int1are subtypes of the corresponding types int2. This rule is usually written as follows, as aninference rule, where the subtyping prop- erties above the horizontal line imply the subtyping property below the line. We read it informally as follows, “If each method typesiis a subtype of method typeti, then the

object type< f1..n : s1..n >is a subtype of the object type< f1..n : t1..n >.”

si<:ti (for eachi∈ {1, . . . , n})

< f1..n : s1..n > <: < f1..n : t1..n >

In general, the method types may include various type constructors for tuples, lists, records, functions, other objects, etc. Each type constructor in OCaml has its own subtyping rules describing how the type construction varies in terms of its component types. These variances can becovariant, meaning that the construction varies in the

CHAPTER 14. OBJECTS14.8. OBJECT TYPES, COERCIONS, AND SUBTYPING same way as a component type;contravariant, meaning the construction varies oppo- sitely to a component type; orinvariant, which means that it is neither purely covariant nor purely contravariant.

For example, consider the tuple type t1 * t2, which is covariant in both types

t1 and t2. If we have two dogs dog * dog, then we also have two animals

animal * animal(sodog * dog <: animal * animal). The inference rule for pairs is specified as follows.

s1<:t1 s2<:s2 (s1∗s2)<:(t1∗t2)

Function subtyping

Nearly all type constructors in OCaml are covariant over all their component types, but there are two exceptions. One is that types that specify mutable values are always invariant. The other exception is more interesting, for the function type. A function typet1 -> t2is covariant in its range typet2, butcontravariantin the domain typet1. This property is written as follows.

t1<:s1 s2<:t2 (s1->s2)<:(t1->t2)

The contravariance in function types is the source of many problems in the design of object-oriented programming languages, and it can be difficult to understand. To get some intuition, consider a functionfeedfor feeding an animal.

# letfeed (x : animal) = x#eat;;

val feed : animal -> unit = <fun>

When calling the function, we can pass it an animal object or a dog object—both

support the eatmethod. Thus, if we like, we can coerce the function to have type dog -> unit.

# letfeed_dog = (feed : animal -> unit :> dog -> unit);;

val feed_dog : dog -> unit = <fun>

Now consider an barking function for dogs. # letdo_bark (x : dog) = x#bark;;

val do_bark : dog -> unit = <fun>

We can’t pass a plainanimalobject todo_bark, because animals do not bark in general. In general, wecannotuse a function of typedog -> unitin places where a function of typeanimal -> unitis expected.

#(do_bark : dog -> unit :> animal -> unit);;

Characters 0-41:

(do_bark : dog -> unit :> animal -> unit);; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Type dog -> unit is not a subtype of type animal -> unit Type animal = < eat : unit > is not a subtype of type

14.9. NARROWING CHAPTER 14. OBJECTS