To: skaller <skaller@maxtal.com.au>
Subject: Re: Thoughts on O'Labl O'Caml merge.
From: John Prevost <prevost@maya.com>
Date: 17 Oct 1999 15:13:35 -0400
In-Reply-To: skaller's message of "Sun, 17 Oct 1999 21:01:37 +1000"
skaller <skaller@maxtal.com.au> writes:
> When a function requires a struct with certain labels,
> we can use a product with 'enough' labels of the right type
> PROVIDED the fields are immuable.
>
> HOWEVER, these rules break down in the presence
> of mutable fields. The rule then is the dual,
... covariant, contravariant, invariant ...
Ahh. Yeah, I'd forgotten this since I'm usually thinking in terms of
immutable values. This gives, I think, greater understanding of why
O'Caml's object system is made more powerful by not allowing object
fields to be accessed outside the object they're part of. (As opposed
to more traditional languages, which allow it within the entire
class.)
Guess that row-polymorphic structs aren't such a good idea, then. :)
An aside about object design and O'Caml:
Something I noted when explaining the O'Caml object system to some
friends was a couple of places where choice of interface can be rather
more subtle than, say, in Java. Specifically in the
point/colored_point, circle/colored_circle examples from the manual
(which I changed a little in my examples).
class point =
object
val mutable x = 0
method get_x = x
method move d = x <- x + d
end
type color =
| Red
| Green
| Blue
class colored_point =
object
val mutable c = Red
method get_color = c
method set_color nc = c <- nc
end
class ['a] circle (center : 'a) =
object
constraint 'a = #point
val mutable center = center
method get_center = center
method set_center x = center <- x
method move = center #move
end
This was my first quick pass (you can tell I wasn't referring to the
manual too much.) To my chagrin, even without the colored_circle
class, this doesn't perform the way you want it to:
# let p = new point
# let cp = new colored_point;;
> val p : point = <obj>
> val cp : colored_point = <obj>
# let c = new circle p
# let cc = new circle cp;;
> val c : point circle = <obj>
> val cc : colored_point circle = <obj>
# (cc :> point circle);;
> Characters 1-3:
> This expression cannot be coerced to type
> point circle =
> < get_center : point; move : int -> unit; set_center : point -> unit >;
> it has type
> colored_point circle =
> < get_center : colored_point; move : int -> unit;
> set_center : colored_point -> unit >
> ... removed
Without even thinking, I chose a poor interface for my circle class.
In fact, the type of colored_point is unfortunate, too. But since I
can't have subtypes of sum types in O'Caml, it's not really a problem.
If I were to use a class for this, or an O'Labl polymorphic variant,
then it becomes an issue.
The real problem in both of these cases is that in a powerfully typed
OO world like O'Caml, it's dangerous to treat a class as a container
when it isn't really. First, it constrains the type overmuch, making
types you'd think to be subtypes not. Second, a major part of what
this constraint is doing in the above case is revealing the
implementation details of the class. I repeat the poor definition of
'a circle with notes and its class type:
# class ['a] circle (center : 'a) =
# object
# constraint 'a = #point
# val mutable center = center
# method get_center = center
# method set_center x = center <- x
# method move = center #move
# end;;
> class ['a] circle : (* invariant 'a *)
> 'a ->
> object
> constraint 'a = #point
> val mutable center : 'a
> method get_center : 'a (* covariant 'a *)
> method set_center : 'a -> unit (* contravariant 'a *)
> method move : int -> unit
> end
Okay, so the obvious symptom of the disease is that 'a appears
covariantly in get_center, and contravariantly in set_center. But
what's the root cause of these symptoms?
Quite simply, I was clever and decided that if there were a get_blah
method, and blah is mutable, there ought to be a set_blah method. Of
course, I was wrong. Or was I?
The other aspect of this definition is that it reveals the type of the
center. This is useful in one respect--we can use it to define
colored_circle:
# class ['a] colored_circle (p : 'a) =
# object
# inherit ['a] circle p
# method get_color = p #get_color
# method set_color = p #set_color
# end
But here's a question: why do we want to expose the fact that the
implementation uses a colored_circle to contain its center?? This may
be a perfectly fine representation, but it's not really reasonable to
expose it in this way.
The model used by the documentation to get around this (though there
was no discussion of what was going on) was to delegate to the point
rather than allow it to be set. But getting the center was still
allowed. I don't think it should have been. Here's are reasonable
class types for circle and colored_circle, assuming that the rest of
things (like how color works) are reasonable:
class type circle :
object
method set_center : point -> unit
method get_center : point
method move : int -> unit
end
class type colored_circle :
object
inherit circle
method set_color : color -> unit
method get_color : color
end
Now. Why are these types appropriate? Don't they restrict the
implementations to use only points? Not in the least! But what they
do do is hide the implementation from the user of the above interface.
The new semantics is that set_center means "move to the same position
as the passed-in point" and get_center means "return a point
representing the position of the center of the circle". Of course,
one must also define whether or not changes in these points will
affect the circle.
Notice that colored_circle does not demand colored_points here. We
can now write:
class colored_circle2 =
object
val mutable p = new point
val mutable c = Red
method set_center p' =
let cx = p #get_x in
let dx = p' #get_x - cx
in p #move dx (* Whoops. Point has no "set" method. *)
method get_center = Oo.clone p
method set_color c' = c <- c'
method get_color = c
end
This shows how something can satisfy the colored_circle interface more
easily now.
Finally, what we really want to say about the circle class type is (in
the latest O'Labl notation):
class type circle :
object
method set_center : 'a . #point as 'a -> unit
method get_center : point
method move : int -> unit
end
This definition shows that anything implementing the same interface as
point may be used. I'm still not entirely sure about this feature of
O'Labl. Having the ability to make polymorphic methods is nice, but
the greater need to specify types of values in O'Labl is rather
upsetting.
So, I think the result of this experience is as follows:
* Treating classes as containers when they're really only delegating
work to another class is dangerous--you expose the nature of the
class you're delegating to, and make it difficult for subtypes of
your class to delegate to different types of objects.
* Sometimes it is appropriate to use a specific type instead of an
object-scope type variable. Especially when you're setting up an
API. This allows you to further hide the implementation details
from API users.
* Finally, method-scope polymorphism makes life simpler since you
can pass subtypes more freely, as you can do with function
polymorphism. (i.e. no more "circ #set_center (cp :> point)", or
at least not as much.)
John.
This archive was generated by hypermail 2b29 : Sun Jan 02 2000 - 11:58:27 MET