Mailing list for all users of the OCaml language and system.
 help / color / mirror / Atom feed
* [Caml-list] Narrowing class's public interface
@ 2004-10-12 22:16 Tony Edgin
  2004-10-13  0:49 ` Jacques Garrigue
  0 siblings, 1 reply; 6+ messages in thread
From: Tony Edgin @ 2004-10-12 22:16 UTC (permalink / raw)
  To: caml-list

What is the reasoning behind Ocaml not allowing the public methods of a class 
to be hidden?  

Some times its nice for a class to have "global" public methods (Java's public 
methods) as well as public methods local to its module (Java's unprefixed 
methods).  This allows for progressive encapsulation, where more distance 
modules no very little about a class, while the more local modules know more 
about the class.  Inheritance could be used to approximate this, but 
sometimes subtyping makes more sense.

I apologize for my lack of clarity.  For those who couldn't understand my 
question, here's an example.

module RandomVariables =
struct
	class type rv_type =
	object
		method instance : float
		method scale_by : float -> rv_type
		method add : rv_type -> rv_type
	end

	class gaussian _stdev _mean =
	object
		method stdev = _stdev
		method mean = _mean

		method instance =  (* elided *)
			(** Returns a draw from the corresponding distribution.
			  *)

		method scale_by a = 
			new gaussian ((abs_float a)*._stdev) (a*._mean)

		method add (other : guassian) = 
			let sigma = sqrt (_stdev**2. +. other#stdev**2.)
			and mu = _mean +. other#mean
			in
				new gaussian sigma mu
	end
	
	(* Truncated *)
end

(* Bad code which I would like to use *)
class gaussian_rv = 
	(RandomVariables.gaussian float -> float -> RandomVariables.rv_type)

;;

cheers.

-- 
Tony Edgin
CARP

-------------------
To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr
Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/
Beginner's list: http://groups.yahoo.com/group/ocaml_beginners


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [Caml-list] Narrowing class's public interface
  2004-10-12 22:16 [Caml-list] Narrowing class's public interface Tony Edgin
@ 2004-10-13  0:49 ` Jacques Garrigue
  2004-10-13  1:43   ` John Prevost
  0 siblings, 1 reply; 6+ messages in thread
From: Jacques Garrigue @ 2004-10-13  0:49 UTC (permalink / raw)
  To: edgin; +Cc: caml-list

From: Tony Edgin <edgin@slingshot.co.nz>

> What is the reasoning behind Ocaml not allowing the public methods
> of a class to be hidden?  

This is just a type soundness problem: as an ocaml object type only
describes the interface of an object, you have no way to make sure
that an object had originally a specific method (with the right type)
once you have hidden it.
Of course we could add some kind of declaration like
   method hidden m : type
and allow a subclass to convert a public method into such an "hidden"
one, but I'm quite sure this is not what you are asking for?
(Note that such an addition would be rather heavy, as "hidden" method
must appear in object types too, i.e. < hidden m : type; ... >)

> Some times its nice for a class to have "global" public methods
> (Java's public methods) as well as public methods local to its
> module (Java's unprefixed methods).  This allows for progressive
> encapsulation, where more distance modules no very little about a
> class, while the more local modules know more about the class.
> Inheritance could be used to approximate this, but sometimes
> subtyping makes more sense.

This problem has been thought of :-)
Look at the user's manual, section 3.17, for how to define "friend"
methods, i.e. methods accessible only from code in the same module.
Note that since ocaml modules are hierarchical, this actually allows
you more precision than Java's packages.

http://wwwfun.kurims.kyoto-u.ac.jp/soft/ocaml/htmlman/manual005.html#ss:friends

Jacques Garrigue

-------------------
To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr
Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/
Beginner's list: http://groups.yahoo.com/group/ocaml_beginners


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [Caml-list] Narrowing class's public interface
  2004-10-13  0:49 ` Jacques Garrigue
@ 2004-10-13  1:43   ` John Prevost
  2004-10-13  2:27     ` skaller
  0 siblings, 1 reply; 6+ messages in thread
From: John Prevost @ 2004-10-13  1:43 UTC (permalink / raw)
  To: caml-list

To expand a bit on what Jacques said, there are a lot of places in
O'Caml where there are very very elegant ways to do things that a
background in more traditional OO languages might not make you think
of.  Here's a version of your original code that highlights just a
couple of those things:

module RandomVariables =
  (struct

    (* First thought: What if we make a generic random_variable class
       that could work for types other than float?  The most generic
       version can only describe its type and the method that returns
       an instance of the RV.  Here we see a class type with a type
       variable, describing a method instance that returns a value of
       that type.  There's no real reason to define this class type,
       except to provide a shorthand--when we say "x random_variable",
       we mean a random variable that returns values of type x. *)

    class type ['a] random_variable =
      object
        method instance : 'a
      end

    (* Of course, the rv_type class that you originally described had
       slightly more functionality.  Specifically, a scale_by method and
       an add method.  The first thing to note here is the type 's, which
       is the "self type" for the object.  An object that is a
       float_random_variable has a method scale_by that takes a float and
       returns a new object with the same type as the original object.
       Likewise, add on an object of type t takes a second object of the
       same type, and returns a new object of the type those objects
       share.  This also shows us that if we want to add arbitrary
       random variables together (not just random variables of the same
       type), we're going to have to do things differently some day.

       The "inherit [float] random_variable" is shorthand for including
       that class type in this class type, specialized for the given
       type "float".  Since O'Caml uses structural subtyping, it's not
       necessary to inherit just to say that this is a subtype.  But it's
       easier than duplicating the definition. *)

    class type float_random_variable =
      object ('s)
        inherit [float] random_variable
        method scale_by : float -> 's
        method add : 's -> 's
      end

    (* This is the trick shown in the O'Caml manual for "friends".  We
       define a new type gaussian_repr, which will carry the "secret value"
       for our gaussian random variables.  We can't use the module type
       to hide the existence of a method for getting the internal
       representation--but we *can* prevent anyone outside this module
       from making use of the value once they get it.  See below in the
       module's signature. *)

    type gaussian_repr =
        { g_stdev : float;
          g_mean : float }

    (* The class is refactored slightly.  Note the use of {< ... >}
       instead of "new gaussian ...".  This ensures that the new object
       is of the same type as the original object.  If you ever want to
       inherit from gaussian to re-use code, this means that the inheriting
       class will work correctly, instead of returning gaussians it will
       return new instances of itself.  Notice that we had to make a field
       repr to hold the internal state, instead of using the constructor's
       closure.  Without the "val repr", we can't use {< ... >} to copy
       the object and change the intenral state of the new copy.

       The "method value" is another part of the friends trick.  Notice
       how we use it in add to get the repr of the other gaussian.  We
       could instead have hidden the return types of the original "stdev"
       and "mean" methods in your version--but I chose to change it to
       this so that it doesn't look too odd.  Remember we can't get rid
       of the methods, so users of the API would wonder "why can't I get
       the stdev and mean out of the gaussian?!?" *)

    class gaussian stdev mean =
      object (_ : 's)
        val repr = { g_stdev = stdev; g_mean = mean }
        method value = repr
        method instance = (*fake*) repr.g_mean
        method scale_by a = {< repr = { g_stdev = (abs_float a) *. stdev;
                                        g_mean = a *. mean } >}
        method add (other : 's) =
          let repr' = other#value in
          let sigma = sqrt (repr.g_stdev ** 2.0 +. repr'.g_stdev ** 2.0) in
          let mu = repr.g_mean +. repr'.g_mean in
          {< repr = { g_stdev = sigma; g_mean = mu } >}
      end

  end : sig

    (* Okay.  Signature time.  The class types are identical. *)

    class type ['a] random_variable =
      object
        method instance : 'a
      end

    class type float_random_variable =
      object ('s)
        inherit [float] random_variable
        method scale_by : float -> 's
        method add : 's -> 's
      end

    (* And here's where we hide the type of the representation.  We don't
       give a type equation after "type gaussian_repr", so the type
       becomes opaque outside of this module. *)

    type gaussian_repr

    (* And now the definition of the gaussian class's type.  Here I just
       inherit all of the methods defined in the float_random_variable
       class type, and then add a new method "value", which returns type
       "gaussian_repr".  The name might be even better
       "gaussian_internal_state" or whatever, to show why the type
       definition is hidden from outside. *)

    class gaussian : float -> float ->
      object
        inherit float_random_variable
        method value : gaussian_repr
      end

  end)

And then we can proceed to use the above, which does compile cleanly.

Now, something to note:

Even though we inherit float_random_variable in the type for class
gaussian, gaussian is not, and never will be, a subtype of
float_random_variable.  The trouble is that we have a binary method
here.  Because add has type 's -> 's, we can never discard any
internal state access methods.  Why not?  Think about what happens if
you put a variety of float_random_variables in a list.  Now you take
the first two items in the list and try to use #add on them.  Say the
first is a gaussian RV and the second is a log random variable.  The
definition of the method add can't deal with this--it will try to hand
the log random variable's state to the internals of the gaussian RV,
which has no idea how to deal with it.

HOWEVER, if we throw away the binary methods, we can in fact put a
collection of "float random_variable" objects in a list.  Here's some
output from an ocaml session:

# let a = new RandomVariables.gaussian 0.0 1.0;;
val a : RandomVariables.gaussian = <obj>
# let b = (a :> RandomVariables.float_random_variable);;
This expression cannot be coerced to type
{ lots of error stuff, which boils down to "can't get rid of that method" }
# let c = (a :> float RandomVariables.random_variable);;
val c : float RandomVariables.random_variable = <obj>

As you can see, casting to float_random_variable didn't work, but
casting to float random_variable worked just fine.  The system is
preventing us from doing something unsound: using a bunch of mixed
float_random_variables together in a way that wouldn't work (because
we can't add them together).  But when we mix them in a way that will
work (we'll only make instances of them), all is well.


The kinds of things that are problemmatic in this system are far
different from the kinds of things that are tricky in traditional OO
systems.  But with some experience, you can see how the system is
amazingly powerful.  I suggest reading not only the section of the
manual on friends, but all of Chapter 3 (Objects in Caml) and Chapter
5 (Advanced examples with classes and modules).

And feel free to ask about suggestions about how to structure your
system.  From the looks of your code, I have some ideas about where
you might be going, and about designs that might work better in a few
places.  If you do ask, make sure to describe your problem in terms of
what you want to do, rather than how you want to do it.  (i.e. "I want
to be able to add arbitrary random variables together", not "I want to
be able to get run-time type information on my random variables so
that I can find the code to add them together in the right way.")

Good luck!

John.

-------------------
To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr
Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/
Beginner's list: http://groups.yahoo.com/group/ocaml_beginners


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [Caml-list] Narrowing class's public interface
  2004-10-13  1:43   ` John Prevost
@ 2004-10-13  2:27     ` skaller
  2004-10-13  6:39       ` John Prevost
  0 siblings, 1 reply; 6+ messages in thread
From: skaller @ 2004-10-13  2:27 UTC (permalink / raw)
  To: John Prevost; +Cc: caml-list

On Wed, 2004-10-13 at 11:43, John Prevost wrote:

> Even though we inherit float_random_variable in the type for class
> gaussian, gaussian is not, and never will be, a subtype of
> float_random_variable.  The trouble is that we have a binary method
> here. 

Indeed. More generally, objects require invariant
(or contravariant) method arguments, but almost all
'real world' problems .. and mathematics ones in particular ..
require covariant arguments, binary operators being
a special case of that.

Hence OO is useless as a general paradigm. It's a useful
technique in special cases .. in particular you get
dynamic dispatch. For example you can use OO abstraction
for device drivers because the read and write methods
only handle a *fixed* data type (characters).

Don't even both trying to make the character type polymorphic
because the theory says it can't be done. The reason is 
also easy to see, as John Provost described, but another
view of the same thing: you'd need one method for every
driver kind/character kind combination. In other words
the number of methods needed is quadratic in the subtypes,
but OO only supports linear -- you can supply a new method
for each derived 'main object' type .. which is why the method
arguments can't also be polymorphic.


-- 
John Skaller, mailto:skaller@users.sf.net
voice: 061-2-9660-0850, 
snail: PO BOX 401 Glebe NSW 2037 Australia
Checkout the Felix programming language http://felix.sf.net



-------------------
To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr
Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/
Beginner's list: http://groups.yahoo.com/group/ocaml_beginners


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [Caml-list] Narrowing class's public interface
  2004-10-13  2:27     ` skaller
@ 2004-10-13  6:39       ` John Prevost
  2004-10-13  7:34         ` skaller
  0 siblings, 1 reply; 6+ messages in thread
From: John Prevost @ 2004-10-13  6:39 UTC (permalink / raw)
  To: caml-list

On 13 Oct 2004 12:27:07 +1000, skaller <skaller@users.sourceforge.net> wrote:
  {...}
> Hence OO is useless as a general paradigm. {...}
  {...}
> Don't even both trying to make the character type polymorphic
> because the theory says it can't be done. The reason is
> also easy to see, as John Provost described, but another
> view of the same thing: you'd need one method for every
> driver kind/character kind combination. In other words
> the number of methods needed is quadratic in the subtypes,
> but OO only supports linear -- you can supply a new method
> for each derived 'main object' type .. which is why the method
> arguments can't also be polymorphic.

Er.  You're overstating the case here.  First, note that OO
programming and subtyping-based polymorphism don't have to go
together.  It's perfectly reasonable, for example, to have a container
class that is invariant in the type of its content.

And your IO example is flawed as well.  The fixed data type is only an
issue if you mix input and output in a single object.  Example:

exception Producer_Empty

class type ['a] producer = object
  method get : unit -> 'a
end

class type ['a] consumer = object
  method put : 'a -> unit
end

class type ['a] producer_consumer = object
  inherit ['a] producer
  inherit ['a] consumer
end

class file_char : filename:string -> char producer_consumer
class file_char_input_stream : filename:string -> char producer
class char_to_line_stream : char producer -> string producer
class map_producer : ('a -> 'b) -> 'a producer -> 'b producer

val iter_over_producer : ('a -> unit) -> 'a producer -> unit

class type bovine = object
  method moo : unit -> unit
end

class type cow = object
  inherit bovine
  method milk : unit -> milk
end

class type supercow = object
  inherit cow
  method fly : unit -> unit
end


val cows : cow producer_consumer


The first thing to notice is that you can have a wide variety of I/O
oriented classes (and functions over those classes, and classes over
those classes) without worrying about the type of the item being
produced or consumed.

The second thing I want to point out is about "cows" above.  cows
cannot be used as a bovine producer_consumer or as a supercow
producer_consumer.  It can, however, be used as either a bovine
producer, or a supercow consumer.    Just because an object type is
invariant does not mean that it cannot be cast to a covariant or
contravariant type.


In other words: you go too far to extend this argument to this level.


When binary methods exist, things are indeed trickier.  They are not,
however, impossible.  Again, remember that casting out the
problemmatic methods can always restrict you down to a type with
simpler constraints.  In the RV example, you can think in terms of a
complicated part of the program which manipulates and creates the RV
objects, fully understanding that when manipulating those objects, the
types must be maintained.  But it can feed the objects it creates into
a collection of restricted RV objects, which only carry the methods
needed to use them, not the methods needed to manipulate them.  No
more binary methods, no problem having them all be the same type.


I have to admit that I don't use the OO features of O'Caml very
much--I generally don't need the flexibility.  But these features are
very very powerful, for a variety of uses.  The trick is to get out of
the *class* oriented mindset, and back into a mindset where the
objects are what matters.  After that, it's all a matter of learning
what constraints type safety imposes, and the right way to work within
those constraints.  There are a few odd cases--like the constructor
issue we saw on this list fairly recently, with the SQL connection
pool--but if you understand the type system, it mostly all just makes
sense.

John.

-------------------
To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr
Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/
Beginner's list: http://groups.yahoo.com/group/ocaml_beginners


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [Caml-list] Narrowing class's public interface
  2004-10-13  6:39       ` John Prevost
@ 2004-10-13  7:34         ` skaller
  0 siblings, 0 replies; 6+ messages in thread
From: skaller @ 2004-10-13  7:34 UTC (permalink / raw)
  To: John Prevost; +Cc: caml-list

On Wed, 2004-10-13 at 16:39, John Prevost wrote:
> On 13 Oct 2004 12:27:07 +1000, skaller <skaller@users.sourceforge.net> wrote:
>   {...}
> > Hence OO is useless as a general paradigm. {...}

> Er.  You're overstating the case here. 

I said OO is useless as general paradigm, 
I meant that and I don't think it is overstated.

I also said objects have a limited use, where
you don't need covariant methods. 

In such cases, you get dynamic (late) binding
which is indeed very useful.

--->
class type supercow = object
  inherit cow
  method fly : unit -> unit
end
<----

... runs out to buy steel umbrella ...

-- 
John Skaller, mailto:skaller@users.sf.net
voice: 061-2-9660-0850, 
snail: PO BOX 401 Glebe NSW 2037 Australia
Checkout the Felix programming language http://felix.sf.net



-------------------
To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr
Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/
Beginner's list: http://groups.yahoo.com/group/ocaml_beginners


^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2004-10-13  7:35 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2004-10-12 22:16 [Caml-list] Narrowing class's public interface Tony Edgin
2004-10-13  0:49 ` Jacques Garrigue
2004-10-13  1:43   ` John Prevost
2004-10-13  2:27     ` skaller
2004-10-13  6:39       ` John Prevost
2004-10-13  7:34         ` skaller

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox