Version française
Home     About     Download     Resources     Contact us    
Browse thread
[Caml-list] Narrowing class's public interface
[ Home ] [ Index: by date | by threads ]
[ Search: ]

[ Message by date: previous | next ] [ Message in thread: previous | next ] [ Thread: previous | next ]
Date: -- (:)
From: John Prevost <j.prevost@g...>
Subject: Re: [Caml-list] Narrowing class's public interface
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